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 — Сохранить аудиофайл**
```python
import os, time
ext = os.path.splitext(original_filename)[1] or ".mp3"
@ -137,87 +133,30 @@ with open(tmp_path, "wb") as f:
**Шаг 2 — Запустить recognition.py**
```bash
python3 ~/.zeroclaw/workspace/skills/ege-checker/recognition.py <tmp_path> --output transcript
python3 ~/.zeroclaw/workspace/skills/ege-checker/recognition.py <tmp_path>
```
**Шаг 3 — Удалить временный файл**
```python
if os.path.exists(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 модуль распознавания аудиофайла с ответами ученика ЕГЭ (аудирование, английский язык).
Зависимости:
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)
"""
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 — описывает формат ответов ученика.
# Критически важна для правильного распознавания "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
# ---------------------------------------------------------------------------
# Транскрипция
# ---------------------------------------------------------------------------
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

@ -1,78 +1,125 @@
# Критерии оценивания раздела "Аудирование" ЕГЭ по английскому языку
# Критерии оценивания раздела "Говорение" ЕГЭ по английскому языку
## Источник: ФИПИ, спецификация ЕГЭ 2026
---
## Структура раздела "Аудирование"
## Структура раздела "Говорение"
Раздел состоит из 9 заданий (задания 19), максимум — 12 первичных баллов.
Каждое задание = 1 балл за верный ответ, 0 за неверный.
Критерии объективные — только совпадение с ключом, никакой субъективной оценки.
4 задания, максимум — 20 первичных баллов.
| Задание | Тип | Макс. баллов |
|---------|-----|-------------|
| 1 | Чтение вслух | 1 |
| 2 | Диалог-расспрос (вопросы) | 4 |
| 3 | Диалог-интервью (ответы) | 5 |
| 4 | Монологическое высказывание | 10 |
| | **Итого** | **20** |
---
## Задание 1 — Установление соответствия (1 задание, 6 баллов)
## Задание 1 — Чтение вслух (01 балл)
Формат: ученик слышит 6 высказываний от 6 говорящих. Нужно установить соответствие
каждого высказывания одному из 7 утверждений (одно утверждение — лишнее).
Ответ: буква AF → цифра 17.
Оценивание: 1 балл за каждое верное соответствие, максимум 6 баллов.
Пример ответа ученика: A3, B1, C5, D7, E2, F4.
| Балл | Критерий |
|------|----------|
| 1 | Речь воспринимается легко: необоснованные паузы отсутствуют; фразовое ударение и интонационные контуры, произношение слов без нарушений нормы. Допускается не более 5 фонетических ошибок, в том числе 12 ошибки, искажающие смысл |
| 0 | Речь воспринимается с трудом из-за большого количества неестественных пауз, запинок, неверной расстановки ударений и ошибок в произношении слов, ИЛИ сделано более 5 фонетических ошибок, ИЛИ сделано 3 и более фонетические ошибки, искажающие смысл |
---
## Задания 23 — Верно / Неверно / В тексте не сказано (2 задания, 2 балла)
## Задание 2 — Диалог-расспрос: вопросы (4 вопроса × 01 балл = 4 балла)
Формат: ученик слышит диалог. По 2 утверждения, нужно определить:
- 1 (True) — утверждение соответствует диалогу
- 2 (False) — утверждение противоречит диалогу
- 3 (Not stated) — информация в диалоге не упоминается
Ученик задаёт 4 вопроса по заданной теме.
Оценивание: 1 балл за каждый верный ответ, максимум 2 балла.
| Балл | Критерий |
|------|----------|
| 1 | Вопрос по содержанию отвечает поставленной задаче; имеет правильную грамматическую форму прямого вопроса; возможные фонетические и лексические погрешности не затрудняют восприятия |
| 0 | Вопрос не задан, ИЛИ заданный вопрос по содержанию не отвечает поставленной задаче, И/ИЛИ не имеет правильной грамматической формы прямого вопроса, И/ИЛИ фонетические и лексические ошибки препятствуют коммуникации |
---
## Задания 49 — Выбор из трёх вариантов (6 заданий, 6 баллов)
## Задание 3 — Диалог-интервью: ответы (5 ответов × 01 балл = 5 баллов)
Формат: ученик слышит 6 монологов/диалогов. К каждому — вопрос с 3 вариантами ответа.
Нужно выбрать верный вариант: 1, 2 или 3.
Ученик отвечает на 5 вопросов интервьюера.
Оценивание: 1 балл за каждый верный ответ, максимум 6 баллов (2 задания по 3 баллов).
> Примечание по структуре: в разных вариантах ФИПИ количество заданий блоков может
> незначительно варьироваться. Агент должен ориентироваться на фактические ключи
> проверяющего, а не на фиксированную нумерацию выше.
| Балл | Критерий |
|------|----------|
| 1 | Дан полный и точный ответ на запрос информации: 23 коммуникативно обусловленные фразы, в которых отсутствуют элементарные лексико-грамматические и/или фонетические ошибки |
| 0 | Ответ на вопрос не дан, ИЛИ содержание ответа не соответствует запросу информации, ИЛИ ответ содержит менее 2 фраз, ИЛИ в ответе имеются элементарные лексико-грамматические и/или фонетические ошибки (в том числе когда ответ носит характер набора слов) |
---
## Итоговая таблица
## Задание 4 — Монологическое высказывание (010 баллов по 3 критериям)
| Блок | Задания | Тип | Макс. баллов |
|------|---------|-----|-------------|
| Соответствие | 1 | AF → 17 | 6 |
| True/False/Not stated | 23 | 1/2/3 | 2 |
| Выбор ответа | 49 | 1/2/3 | 6 (иногда делится на подблоки) |
| | | **Итого** | **12** |
Тематическое монологическое высказывание высокого уровня с элементами описания и рассуждения.
### Критерий 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. Распознанные ответы ученика (из аудиозаписи через recognition.py)
2. Правильные ответы (ключи) от проверяющего
1. Аудиозапись ответа ученика (транскрибируется через recognition.py)
2. Задание (текст для чтения вслух, тема интервью, план монолога и т.д.)
### Логика сверки
- Задание 1: совпадение буква → цифра (A=3 у ученика, A=3 в ключе → 1 балл)
- Задания 29: совпадение цифры (ученик: 1, ключ: 1 → 1 балл)
- Нераспознанный ответ ("?") → 0 баллов
- Регистр не важен: "TRUE", "true", "T" — одно и то же
- Для соответствий принимать форматы: "A3", "A-3", "A=3", "A — 3"
### Логика оценки по заданиям
**Задание 1 (чтение вслух):**
- Считать фонетические ошибки по транскрипту
- Отметить паузы и запинки если есть
- Вынести 0 или 1 балл с обоснованием
**Задание 2 (вопросы):**
- Оценить каждый из 4 вопросов отдельно (0 или 1)
- Проверить: соответствие теме + грамматическая форма прямого вопроса + понятность
**Задание 3 (ответы):**
- Оценить каждый из 5 ответов отдельно (0 или 1)
- Проверить: полнота (23 фразы) + соответствие вопросу + отсутствие грубых ошибок
**Задание 4 (монолог):**
- К1: посчитать аспекты из задания, проверить раскрытие каждого + подсчитать фразы
- К2: проверить наличие вступления с обращением, заключения, логических связок
- К3: посчитать лексико-грамматические и фонетические ошибки, разделить на грубые/негрубые
### Формат вывода
Три блока с таблицами: задание | ответ ученика | ключ | результат (+ или -)
Итог по каждому блоку + общий итог из 12.
Список ошибок с пояснением: "Задание 1B: ученик — 5, верный ответ — 1".
Если есть нераспознанные задания — явно указать какие и что они засчитаны как 0.
Для каждого задания: балл + краткое обоснование со ссылкой на критерий.
Итоговая таблица: задание | балл | макс.
Общий итог из 20.
При наличии ошибок — конкретные примеры из транскрипта.