update english criterias, skill

This commit is contained in:
shuler7 2026-04-30 10:33:39 +03:00
parent 4764b4cb6e
commit b263661b9f
7 changed files with 427 additions and 677 deletions

View file

@ -19,7 +19,7 @@ description: >
| Модуль | Вход — только распознавание | Вход — распознавание + оценка | | Модуль | Вход — только распознавание | Вход — распознавание + оценка |
|--------|----------------------------|-------------------------------| |--------|----------------------------|-------------------------------|
| Сочинение (русский) | Сканы бланков | Сканы + исходный текст + тема | | Сочинение (русский) | Сканы бланков | Сканы + исходный текст + тема |
| Аудирование (английский) | Аудиозапись | Аудиозапись + ключи проверяющего | | Аудирование (английский) | Аудиозапись | Аудиозапись |
--- ---
@ -114,18 +114,14 @@ description: >
### Логика запуска ### Логика запуска
**Если прислали только аудиозапись (без ключей):** **Если прислали аудиозапись:**
→ Только распознать ответы ученика через recognition.py и вывести их таблицей. → Распознать ответы ученика через recognition.py и вывести их списком.
→ Сообщить: "Ответы распознаны. Чтобы выставить баллы, пришлите правильные ответы (ключи)." → Сверить полученные ответы по критериям и сообщить баллы.
**Если прислали аудиозапись + ключи проверяющего:**
→ Распознать ответы, затем сверить с ключами и выставить баллы с объяснением ошибок.
--- ---
### Режим 1: Только распознавание
**Шаг 1 — Сохранить аудиофайл** **Шаг 1 — Сохранить аудиофайл**
```python ```python
import os, time import os, time
ext = os.path.splitext(original_filename)[1] or ".mp3" ext = os.path.splitext(original_filename)[1] or ".mp3"
@ -137,87 +133,30 @@ with open(tmp_path, "wb") as f:
**Шаг 2 — Запустить recognition.py** **Шаг 2 — Запустить recognition.py**
```bash ```bash
python3 ~/.zeroclaw/workspace/skills/ege-checker/recognition.py <tmp_path> --output transcript python3 ~/.zeroclaw/workspace/skills/ege-checker/recognition.py <tmp_path>
``` ```
**Шаг 3 — Удалить временный файл** **Шаг 3 — Удалить временный файл**
```python ```python
if os.path.exists(tmp_path): if os.path.exists(tmp_path):
os.remove(tmp_path) os.remove(tmp_path)
``` ```
**Шаг 4 — Вывести распознанные ответы** **Шаг 4 - Загрузить критерии**
Таблица: задание | распознанный ответ
Прочитай `references/english-listening-criteria.md` — критерии 1-4 задания с баллами (ЕГЭ 2026).
**Шаг 5 — Оценить задания по критериям**
Для каждого критерия:
- Объясни снятие баллов — что именно не выполнено и почему
- Выставь балл
**Шаг 6 — Итоговый вывод**
Список: задание - распознанный ответ - выставленный балл - обьяснение оценки
Если есть нераспознанные — явно отметить. Если есть нераспознанные — явно отметить.
Сообщить: "Ответы распознаны. Пришлите правильные ответы (ключи) для выставления баллов."
---
### Режим 2: Распознавание + оценка
**Шаги 13** — те же, что в режиме 1 (сохранить, запустить, удалить).
**Шаг 4 — Получить и распознать ключи**
Если ключи текстом — использовать напрямую.
Если ключи фото — распознать через vision.
**Шаг 5 — Загрузить критерии**
Прочитай `references/english-listening-criteria.md`.
**Шаг 6 — Передать в LLM для сверки**
Промпт для модели (подставить реальные значения):
```
Ты эксперт-проверяющий ЕГЭ по английскому (аудирование, 2026).
Транскрипт ответов ученика (Whisper):
[TRANSCRIPT]
Извлечённые ответы ученика:
- Задание 1 (AF): [TASK1]
- Задания 23 (True/False/Not stated): [TASK2_3]
- Задания 49 (выбор 1/2/3): [TASK4_9]
- Не распознаны (засчитать как 0): [UNRECOGNIZED]
Правильные ответы (ключи):
[KEYS]
Сверь ответы с ключами. 1 балл за совпадение, 0 за несовпадение или отсутствие.
Верни строго JSON без markdown:
{"results":{"task1":{"A":true},"task2_3":{"2":true},"task4_9":{"4":false}},
"scores":{"task1":0,"task2_3":0,"task4_9":0,"total":0},
"errors":["Задание 1B: ученик — 5, верный ответ — 1"]}
```
**Шаг 7 — Вывести результат**
```
## Результаты: Аудирование ЕГЭ (Английский язык, 2026)
### Задание 1 — Установление соответствия (макс. 6 баллов)
| Высказывание | Ответ ученика | Ключ | Результат |
| A | 3 | 3 | + |
| B | 5 | 1 | - |
...
Баллов: X / 6
### Задания 23 — True / False / Not stated (макс. 2 балла)
...
Баллов: X / 2
### Задания 49 — Выбор ответа (макс. 6 баллов)
...
Баллов: X / 6 (или X / подблок)
### Итого: XX / 12
### Ошибки с пояснением:
- Задание 1B: ученик ответил 5, верный ответ 1
...
[Если были нераспознанные:]
Задания X, Y не были распознаны в аудио и засчитаны как неверные (0 баллов).
```
--- ---

View file

@ -1,503 +1,210 @@
""" """
recognition.py модуль распознавания аудиофайла с ответами ученика ЕГЭ (аудирование, английский язык). recognition.py модуль распознавания аудиофайла с ответами ученика ЕГЭ (аудирование, английский язык).
Зависимости: Зависимости:
pip install faster-whisper pip install faster-whisper
"""
Использование:
from recognition import transcribe, extract_answers from __future__ import annotations
# Полный pipeline: аудио -> транскрипт -> структурированные ответы import re
result = transcribe("student_answers.mp3") import logging
answers = extract_answers(result.text) from dataclasses import dataclass, field
print(answers) from pathlib import Path
"""
logger = logging.getLogger(__name__)
from __future__ import annotations
# ---------------------------------------------------------------------------
import re # Константы
import logging # ---------------------------------------------------------------------------
from dataclasses import dataclass, field
from pathlib import Path # Модели faster-whisper по убыванию скорости / возрастанию качества:
# tiny, base, small, medium, large-v2, large-v3
logger = logging.getLogger(__name__) DEFAULT_MODEL = "medium"
# --------------------------------------------------------------------------- # Подсказка для Whisper — описывает формат ответов ученика.
# Константы # Критически важна для правильного распознавания "one/two/three" как цифр
# --------------------------------------------------------------------------- # и "A equals 3" как ответа на задание 1.
WHISPER_PROMPT = (
# Модели faster-whisper по убыванию скорости / возрастанию качества: "Student answers to EGE English listening exam. "
# tiny, base, small, medium, large-v2, large-v3 "Task one matching: speaker A answer three, speaker B answer one, "
DEFAULT_MODEL = "medium" "speaker C answer five, speaker D answer seven, speaker E answer two, speaker F answer four. "
"Tasks two through nine True False Not Stated: "
# Подсказка для Whisper — описывает формат ответов ученика. "task two true, task three false, task four not stated. "
# Критически важна для правильного распознавания "one/two/three" как цифр "Tasks ten through eighteen multiple choice one two or three: "
# и "A equals 3" как ответа на задание 1. "task ten two, task eleven one, task twelve three."
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 TranscriptResult:
"""Результат транскрипции аудиофайла.""" # ---------------------------------------------------------------------------
text: str # Полный текст транскрипта # Транскрипция
language: str # Определённый язык ("en") # ---------------------------------------------------------------------------
duration_seconds: float # Длительность аудио в секундах
segments: list[dict] = field(default_factory=list) # Детальные сегменты с таймкодами def transcribe(
model_used: str = DEFAULT_MODEL audio_path: str | Path,
model_size: str = DEFAULT_MODEL,
device: str = "auto",
@dataclass compute_type: str = "auto",
class StudentAnswers: language: str = "en",
"""Структурированные ответы ученика, извлечённые из транскрипта.""" beam_size: int = 5,
) -> TranscriptResult:
# Задание 1: соответствие AF → цифра 17 """
task1: dict[str, str] = field(default_factory=dict) # {"A": "3", "B": "1", ...} Транскрибирует аудиофайл с ответами ученика.
# Задания 29: True(1) / False(2) / Not Stated(3) Args:
task2_9: dict[int, str] = field(default_factory=dict) # {2: "1", 3: "2", ...} audio_path: Путь к аудиофайлу (MP3, WAV, M4A, OGG, WEBM, FLAC).
model_size: Размер модели Whisper: tiny/base/small/medium/large-v2/large-v3.
# Задания 1018: выбор из вариантов 1/2/3 medium хороший баланс скорость/качество для ЕГЭ.
task10_18: dict[int, str] = field(default_factory=dict) # {10: "2", 11: "1", ...} large-v3 максимальное качество, медленнее.
device: "auto" | "cpu" | "cuda". "auto" выберет GPU если доступен.
# Задания, которые не удалось распознать compute_type: "auto" | "int8" | "float16" | "float32".
unrecognized: list[str] = field(default_factory=list) "auto" подберёт оптимальный тип для устройства.
language: Язык аудио. "en" для ответов на английском.
def to_dict(self) -> dict: beam_size: Ширина луча beam search. 5 стандарт, выше = точнее но медленнее.
return {
"task1": self.task1, Returns:
"task2_9": {str(k): v for k, v in self.task2_9.items()}, TranscriptResult с текстом, языком, длительностью и сегментами.
"task10_18": {str(k): v for k, v in self.task10_18.items()},
"unrecognized": self.unrecognized, Raises:
} FileNotFoundError: Если аудиофайл не найден.
RuntimeError: Если faster-whisper не установлен.
def summary(self) -> str: """
"""Читаемое представление для вывода агенту / в лог.""" try:
lines = ["=== Распознанные ответы ученика ==="] from faster_whisper import WhisperModel
except ImportError:
if self.task1: raise RuntimeError(
lines.append("\nЗадание 1 (соответствие):") "faster-whisper не установлен. Установите: pip install faster-whisper"
for letter in "ABCDEF": )
ans = self.task1.get(letter, "?")
lines.append(f" {letter}{ans}") audio_path = Path(audio_path)
if not audio_path.exists():
if self.task2_9: raise FileNotFoundError(f"Аудиофайл не найден: {audio_path}")
lines.append("\nЗадания 29 (True/False/Not Stated):")
labels = {"1": "True", "2": "False", "3": "Not Stated"} # Автовыбор устройства и типа вычислений
for task_num in range(2, 10): resolved_device, resolved_compute = _resolve_device(device, compute_type)
ans = self.task2_9.get(task_num, "?")
label = labels.get(ans, ans) logger.info(
lines.append(f" Задание {task_num}: {ans} ({label})") "Загрузка модели %s на %s (%s)...",
model_size, resolved_device, resolved_compute
if self.task10_18: )
lines.append("\nЗадания 1018 (выбор):")
for task_num in range(10, 19): model = WhisperModel(
ans = self.task10_18.get(task_num, "?") model_size,
lines.append(f" Задание {task_num}: {ans}") device=resolved_device,
compute_type=resolved_compute,
if self.unrecognized: )
lines.append(f"\nНе распознано: {', '.join(self.unrecognized)}")
logger.info("Транскрибирую: %s", audio_path.name)
return "\n".join(lines)
segments_gen, info = model.transcribe(
str(audio_path),
# --------------------------------------------------------------------------- language=language,
# Транскрипция beam_size=beam_size,
# --------------------------------------------------------------------------- initial_prompt=WHISPER_PROMPT,
word_timestamps=False,
def transcribe( vad_filter=True, # Фильтрация тишины — полезно для записей с паузами
audio_path: str | Path, vad_parameters={
model_size: str = DEFAULT_MODEL, "min_silence_duration_ms": 500, # Паузы >0.5с считаются тишиной
device: str = "auto", "speech_pad_ms": 200,
compute_type: str = "auto", },
language: str = "en", )
beam_size: int = 5,
) -> TranscriptResult: # Материализуем генератор сегментов
""" segments = []
Транскрибирует аудиофайл с ответами ученика. full_text_parts = []
Args: for seg in segments_gen:
audio_path: Путь к аудиофайлу (MP3, WAV, M4A, OGG, WEBM, FLAC). segments.append({
model_size: Размер модели Whisper: tiny/base/small/medium/large-v2/large-v3. "start": round(seg.start, 2),
medium хороший баланс скорость/качество для ЕГЭ. "end": round(seg.end, 2),
large-v3 максимальное качество, медленнее. "text": seg.text.strip(),
device: "auto" | "cpu" | "cuda". "auto" выберет GPU если доступен. })
compute_type: "auto" | "int8" | "float16" | "float32". full_text_parts.append(seg.text.strip())
"auto" подберёт оптимальный тип для устройства.
language: Язык аудио. "en" для ответов на английском. full_text = " ".join(full_text_parts)
beam_size: Ширина луча beam search. 5 стандарт, выше = точнее но медленнее.
logger.info(
Returns: "Транскрипция завершена. Длительность: %.1f сек, слов ~%d",
TranscriptResult с текстом, языком, длительностью и сегментами. info.duration, len(full_text.split())
)
Raises:
FileNotFoundError: Если аудиофайл не найден. return TranscriptResult(
RuntimeError: Если faster-whisper не установлен. text=full_text,
""" language=info.language,
try: duration_seconds=round(info.duration, 1),
from faster_whisper import WhisperModel segments=segments,
except ImportError: model_used=model_size,
raise RuntimeError( )
"faster-whisper не установлен. Установите: pip install faster-whisper"
)
def _resolve_device(device: str, compute_type: str) -> tuple[str, str]:
audio_path = Path(audio_path) """Определяет оптимальное устройство и тип вычислений."""
if not audio_path.exists(): if device != "auto" and compute_type != "auto":
raise FileNotFoundError(f"Аудиофайл не найден: {audio_path}") return device, compute_type
# Автовыбор устройства и типа вычислений # Проверяем наличие CUDA
resolved_device, resolved_compute = _resolve_device(device, compute_type) try:
from torch import cuda
logger.info( has_cuda = cuda.is_available()
"Загрузка модели %s на %s (%s)...", except ImportError:
model_size, resolved_device, resolved_compute has_cuda = False
)
if device == "auto":
model = WhisperModel( device = "cuda" if has_cuda else "cpu"
model_size,
device=resolved_device, if compute_type == "auto":
compute_type=resolved_compute, if device == "cuda":
) compute_type = "float16" # GPU: float16 быстрее и точнее чем int8
else:
logger.info("Транскрибирую: %s", audio_path.name) compute_type = "int8" # CPU: int8 значительно быстрее float32
segments_gen, info = model.transcribe( return device, compute_type
str(audio_path),
language=language,
beam_size=beam_size, # ---------------------------------------------------------------------------
initial_prompt=WHISPER_PROMPT, # CLI
word_timestamps=False, # ---------------------------------------------------------------------------
vad_filter=True, # Фильтрация тишины — полезно для записей с паузами
vad_parameters={ if __name__ == "__main__":
"min_silence_duration_ms": 500, # Паузы >0.5с считаются тишиной import argparse
"speech_pad_ms": 200, import sys
},
) logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
# Материализуем генератор сегментов parser = argparse.ArgumentParser(
segments = [] description="Распознавание аудиоответов ЕГЭ (аудирование, английский язык)"
full_text_parts = [] )
parser.add_argument("audio", help="Путь к аудиофайлу")
for seg in segments_gen: parser.add_argument(
segments.append({ "--model", default=DEFAULT_MODEL,
"start": round(seg.start, 2), choices=["tiny", "base", "small", "medium", "large-v2", "large-v3"],
"end": round(seg.end, 2), help=f"Размер модели Whisper (по умолчанию: {DEFAULT_MODEL})"
"text": seg.text.strip(), )
}) parser.add_argument(
full_text_parts.append(seg.text.strip()) "--device", default="auto",
choices=["auto", "cpu", "cuda"],
full_text = " ".join(full_text_parts) help="Устройство для инференса (по умолчанию: auto)"
)
logger.info(
"Транскрипция завершена. Длительность: %.1f сек, слов ~%d", args = parser.parse_args()
info.duration, len(full_text.split())
) try:
result = transcribe(args.audio, model_size=args.model, device=args.device)
return TranscriptResult( print(result.text)
text=full_text, except (FileNotFoundError, RuntimeError) as e:
language=info.language, print(f"Ошибка: {e}", file=sys.stderr)
duration_seconds=round(info.duration, 1), sys.exit(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,78 +1,125 @@
# Критерии оценивания раздела "Аудирование" ЕГЭ по английскому языку # Критерии оценивания раздела "Говорение" ЕГЭ по английскому языку
## Источник: ФИПИ, спецификация ЕГЭ 2026 ## Источник: ФИПИ, спецификация ЕГЭ 2026
--- ---
## Структура раздела "Аудирование" ## Структура раздела "Говорение"
Раздел состоит из 9 заданий (задания 19), максимум — 12 первичных баллов. 4 задания, максимум — 20 первичных баллов.
Каждое задание = 1 балл за верный ответ, 0 за неверный.
Критерии объективные — только совпадение с ключом, никакой субъективной оценки. | Задание | Тип | Макс. баллов |
|---------|-----|-------------|
| 1 | Чтение вслух | 1 |
| 2 | Диалог-расспрос (вопросы) | 4 |
| 3 | Диалог-интервью (ответы) | 5 |
| 4 | Монологическое высказывание | 10 |
| | **Итого** | **20** |
--- ---
## Задание 1 — Установление соответствия (1 задание, 6 баллов) ## Задание 1 — Чтение вслух (01 балл)
Формат: ученик слышит 6 высказываний от 6 говорящих. Нужно установить соответствие | Балл | Критерий |
каждого высказывания одному из 7 утверждений (одно утверждение — лишнее). |------|----------|
| 1 | Речь воспринимается легко: необоснованные паузы отсутствуют; фразовое ударение и интонационные контуры, произношение слов без нарушений нормы. Допускается не более 5 фонетических ошибок, в том числе 12 ошибки, искажающие смысл |
Ответ: буква AF → цифра 17. | 0 | Речь воспринимается с трудом из-за большого количества неестественных пауз, запинок, неверной расстановки ударений и ошибок в произношении слов, ИЛИ сделано более 5 фонетических ошибок, ИЛИ сделано 3 и более фонетические ошибки, искажающие смысл |
Оценивание: 1 балл за каждое верное соответствие, максимум 6 баллов.
Пример ответа ученика: A3, B1, C5, D7, E2, F4.
--- ---
## Задания 23 — Верно / Неверно / В тексте не сказано (2 задания, 2 балла) ## Задание 2 — Диалог-расспрос: вопросы (4 вопроса × 01 балл = 4 балла)
Формат: ученик слышит диалог. По 2 утверждения, нужно определить: Ученик задаёт 4 вопроса по заданной теме.
- 1 (True) — утверждение соответствует диалогу
- 2 (False) — утверждение противоречит диалогу
- 3 (Not stated) — информация в диалоге не упоминается
Оценивание: 1 балл за каждый верный ответ, максимум 2 балла. | Балл | Критерий |
|------|----------|
| 1 | Вопрос по содержанию отвечает поставленной задаче; имеет правильную грамматическую форму прямого вопроса; возможные фонетические и лексические погрешности не затрудняют восприятия |
| 0 | Вопрос не задан, ИЛИ заданный вопрос по содержанию не отвечает поставленной задаче, И/ИЛИ не имеет правильной грамматической формы прямого вопроса, И/ИЛИ фонетические и лексические ошибки препятствуют коммуникации |
--- ---
## Задания 49 — Выбор из трёх вариантов (6 заданий, 6 баллов) ## Задание 3 — Диалог-интервью: ответы (5 ответов × 01 балл = 5 баллов)
Формат: ученик слышит 6 монологов/диалогов. К каждому — вопрос с 3 вариантами ответа. Ученик отвечает на 5 вопросов интервьюера.
Нужно выбрать верный вариант: 1, 2 или 3.
Оценивание: 1 балл за каждый верный ответ, максимум 6 баллов (2 задания по 3 баллов). | Балл | Критерий |
|------|----------|
> Примечание по структуре: в разных вариантах ФИПИ количество заданий блоков может | 1 | Дан полный и точный ответ на запрос информации: 23 коммуникативно обусловленные фразы, в которых отсутствуют элементарные лексико-грамматические и/или фонетические ошибки |
> незначительно варьироваться. Агент должен ориентироваться на фактические ключи | 0 | Ответ на вопрос не дан, ИЛИ содержание ответа не соответствует запросу информации, ИЛИ ответ содержит менее 2 фраз, ИЛИ в ответе имеются элементарные лексико-грамматические и/или фонетические ошибки (в том числе когда ответ носит характер набора слов) |
> проверяющего, а не на фиксированную нумерацию выше.
--- ---
## Итоговая таблица ## Задание 4 — Монологическое высказывание (010 баллов по 3 критериям)
| Блок | Задания | Тип | Макс. баллов | Тематическое монологическое высказывание высокого уровня с элементами описания и рассуждения.
|------|---------|-----|-------------|
| Соответствие | 1 | AF → 17 | 6 | ### Критерий 1: Решение коммуникативной задачи / Содержание (04 балла)
| True/False/Not stated | 23 | 1/2/3 | 2 |
| Выбор ответа | 49 | 1/2/3 | 6 (иногда делится на подблоки) | | Балл | Критерий |
| | | **Итого** | **12** | |------|----------|
| 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. Распознанные ответы ученика (из аудиозаписи через recognition.py) 1. Аудиозапись ответа ученика (транскрибируется через recognition.py)
2. Правильные ответы (ключи) от проверяющего 2. Задание (текст для чтения вслух, тема интервью, план монолога и т.д.)
### Логика сверки ### Логика оценки по заданиям
- Задание 1: совпадение буква → цифра (A=3 у ученика, A=3 в ключе → 1 балл)
- Задания 29: совпадение цифры (ученик: 1, ключ: 1 → 1 балл) **Задание 1 (чтение вслух):**
- Нераспознанный ответ ("?") → 0 баллов - Считать фонетические ошибки по транскрипту
- Регистр не важен: "TRUE", "true", "T" — одно и то же - Отметить паузы и запинки если есть
- Для соответствий принимать форматы: "A3", "A-3", "A=3", "A — 3" - Вынести 0 или 1 балл с обоснованием
**Задание 2 (вопросы):**
- Оценить каждый из 4 вопросов отдельно (0 или 1)
- Проверить: соответствие теме + грамматическая форма прямого вопроса + понятность
**Задание 3 (ответы):**
- Оценить каждый из 5 ответов отдельно (0 или 1)
- Проверить: полнота (23 фразы) + соответствие вопросу + отсутствие грубых ошибок
**Задание 4 (монолог):**
- К1: посчитать аспекты из задания, проверить раскрытие каждого + подсчитать фразы
- К2: проверить наличие вступления с обращением, заключения, логических связок
- К3: посчитать лексико-грамматические и фонетические ошибки, разделить на грубые/негрубые
### Формат вывода ### Формат вывода
Три блока с таблицами: задание | ответ ученика | ключ | результат (+ или -) Для каждого задания: балл + краткое обоснование со ссылкой на критерий.
Итог по каждому блоку + общий итог из 12. Итоговая таблица: задание | балл | макс.
Список ошибок с пояснением: "Задание 1B: ученик — 5, верный ответ — 1". Общий итог из 20.
Если есть нераспознанные задания — явно указать какие и что они засчитаны как 0. При наличии ошибок — конкретные примеры из транскрипта.

View file

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

View file

@ -21,7 +21,7 @@ def transcribe_file(
text=True, text=True,
check=True, check=True,
encoding="utf-8", encoding="utf-8",
timeout=timeout_seconds, # Добавлен таймаут timeout=timeout_seconds,
) )
# Предполагаем, что скрипт выводит транскрипцию в stdout # Предполагаем, что скрипт выводит транскрипцию в stdout
transcript = result.stdout.strip() transcript = result.stdout.strip()
@ -41,10 +41,10 @@ def transcribe_file(
def main(): def main():
# Пути # Пути
base_dir = Path( base_dir = Path(
r"/mnt/c/proga/projects/ege-skill-dev/.scans/Сложные работы_Английский_язык_УЧ_часть_1/" r"/mnt/c/proga/projects/ege-skill-dev/.scans/Сложные работы_Английский язык УЧ часть 1/"
) )
regions = ["1-Республика_Адыгея", "2-Республикаашкортостан"] regions = ["1-Республика Адыгея"]
recognizer_script = Path.home() / "zeroclaw-bot" / "recognition.py" recognizer_script = Path.home() / ".zeroclaw" / "workspace" / "skills" / "ege-checker" / "recognition.py"
# Таймаут на транскрипцию одного файла (в секундах) # Таймаут на транскрипцию одного файла (в секундах)
TIMEOUT_PER_FILE = 120 TIMEOUT_PER_FILE = 120
@ -109,8 +109,15 @@ def main():
# Сохраняем JSON # Сохраняем JSON
output_file = Path.cwd() / "transcriptions.json" output_file = Path.cwd() / "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: with open(output_file, "w", encoding="utf-8") as f:
json.dump(results, f, ensure_ascii=False, indent=2) json.dump(results_to_file, f, ensure_ascii=False, indent=2)
# Статистика # Статистика
total_files = len(results) total_files = len(results)

File diff suppressed because one or more lines are too long

50
transcriptions.json Normal file
View file

@ -0,0 +1,50 @@
[
{
"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.",
"file_path": "/mnt/c/proga/projects/ege-skill-dev/.scans/Сложные работы_Английский язык УЧ часть 1/1-Республика Адыгея/Кузьминова_7921829566/audio_497920_1.ogg"
},
{
"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?",
"file_path": "/mnt/c/proga/projects/ege-skill-dev/.scans/Сложные работы_Английский язык УЧ часть 1/1-Республика Адыгея/Кузьминова_7921829566/audio_497920_2.ogg"
},
{
"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.",
"file_path": "/mnt/c/proga/projects/ege-skill-dev/.scans/Сложные работы_Английский язык УЧ часть 1/1-Республика Адыгея/Кузьминова_7921829566/audio_497920_3.ogg"
},
{
"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!",
"file_path": "/mnt/c/proga/projects/ege-skill-dev/.scans/Сложные работы_Английский язык УЧ часть 1/1-Республика Адыгея/Кузьминова_7921829566/audio_497920_4.ogg"
},
{
"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.",
"file_path": "/mnt/c/proga/projects/ege-skill-dev/.scans/Сложные работы_Английский язык УЧ часть 1/1-Республика Адыгея/Штельмах_7924913136/audio_497976_1.ogg"
},
{
"region": "1-Республика Адыгея",
"fio": "Штельмах_7924913136",
"transcript": "What variety of jobs you have to start there, working, pay as much as you pay per hour.",
"file_path": "/mnt/c/proga/projects/ege-skill-dev/.scans/Сложные работы_Английский язык УЧ часть 1/1-Республика Адыгея/Штельмах_7924913136/audio_497976_2.ogg"
},
{
"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.",
"file_path": "/mnt/c/proga/projects/ege-skill-dev/.scans/Сложные работы_Английский язык УЧ часть 1/1-Республика Адыгея/Штельмах_7924913136/audio_497976_3.ogg"
},
{
"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.",
"file_path": "/mnt/c/proga/projects/ege-skill-dev/.scans/Сложные работы_Английский язык УЧ часть 1/1-Республика Адыгея/Штельмах_7924913136/audio_497976_4.ogg"
}
]