update english criterias, skill
This commit is contained in:
parent
4764b4cb6e
commit
b263661b9f
7 changed files with 427 additions and 677 deletions
|
|
@ -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: Распознавание + оценка
|
||||
|
||||
**Шаги 1–3** — те же, что в режиме 1 (сохранить, запустить, удалить).
|
||||
|
||||
**Шаг 4 — Получить и распознать ключи**
|
||||
Если ключи текстом — использовать напрямую.
|
||||
Если ключи фото — распознать через vision.
|
||||
|
||||
**Шаг 5 — Загрузить критерии**
|
||||
Прочитай `references/english-listening-criteria.md`.
|
||||
|
||||
**Шаг 6 — Передать в LLM для сверки**
|
||||
|
||||
Промпт для модели (подставить реальные значения):
|
||||
```
|
||||
Ты эксперт-проверяющий ЕГЭ по английскому (аудирование, 2026).
|
||||
|
||||
Транскрипт ответов ученика (Whisper):
|
||||
[TRANSCRIPT]
|
||||
|
||||
Извлечённые ответы ученика:
|
||||
- Задание 1 (A–F): [TASK1]
|
||||
- Задания 2–3 (True/False/Not stated): [TASK2_3]
|
||||
- Задания 4–9 (выбор 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
|
||||
|
||||
### Задания 2–3 — True / False / Not stated (макс. 2 балла)
|
||||
...
|
||||
Баллов: X / 2
|
||||
|
||||
### Задания 4–9 — Выбор ответа (макс. 6 баллов)
|
||||
...
|
||||
Баллов: X / 6 (или X / подблок)
|
||||
|
||||
### Итого: XX / 12
|
||||
|
||||
### Ошибки с пояснением:
|
||||
- Задание 1B: ученик ответил 5, верный ответ 1
|
||||
...
|
||||
|
||||
[Если были нераспознанные:]
|
||||
Задания X, Y не были распознаны в аудио и засчитаны как неверные (0 баллов).
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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: соответствие A–F → цифра 1–7
|
||||
task1: dict[str, str] = field(default_factory=dict) # {"A": "3", "B": "1", ...}
|
||||
|
||||
# Задания 2–9: True(1) / False(2) / Not Stated(3)
|
||||
task2_9: dict[int, str] = field(default_factory=dict) # {2: "1", 3: "2", ...}
|
||||
|
||||
# Задания 10–18: выбор из вариантов 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Задания 2–9 (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Задания 10–18 (выбор):")
|
||||
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: соответствие A–F → цифра 1–7.
|
||||
Примеры: "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:
|
||||
"""
|
||||
Задания 2–9: 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:
|
||||
"""
|
||||
Задания 10–18: выбор из вариантов 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)
|
||||
|
|
|
|||
|
|
@ -1,78 +1,125 @@
|
|||
# Критерии оценивания раздела "Аудирование" ЕГЭ по английскому языку
|
||||
# Критерии оценивания раздела "Говорение" ЕГЭ по английскому языку
|
||||
## Источник: ФИПИ, спецификация ЕГЭ 2026
|
||||
|
||||
---
|
||||
|
||||
## Структура раздела "Аудирование"
|
||||
## Структура раздела "Говорение"
|
||||
|
||||
Раздел состоит из 9 заданий (задания 1–9), максимум — 12 первичных баллов.
|
||||
Каждое задание = 1 балл за верный ответ, 0 за неверный.
|
||||
Критерии объективные — только совпадение с ключом, никакой субъективной оценки.
|
||||
4 задания, максимум — 20 первичных баллов.
|
||||
|
||||
| Задание | Тип | Макс. баллов |
|
||||
|---------|-----|-------------|
|
||||
| 1 | Чтение вслух | 1 |
|
||||
| 2 | Диалог-расспрос (вопросы) | 4 |
|
||||
| 3 | Диалог-интервью (ответы) | 5 |
|
||||
| 4 | Монологическое высказывание | 10 |
|
||||
| | **Итого** | **20** |
|
||||
|
||||
---
|
||||
|
||||
## Задание 1 — Установление соответствия (1 задание, 6 баллов)
|
||||
## Задание 1 — Чтение вслух (0–1 балл)
|
||||
|
||||
Формат: ученик слышит 6 высказываний от 6 говорящих. Нужно установить соответствие
|
||||
каждого высказывания одному из 7 утверждений (одно утверждение — лишнее).
|
||||
|
||||
Ответ: буква A–F → цифра 1–7.
|
||||
Оценивание: 1 балл за каждое верное соответствие, максимум 6 баллов.
|
||||
Пример ответа ученика: A–3, B–1, C–5, D–7, E–2, F–4.
|
||||
| Балл | Критерий |
|
||||
|------|----------|
|
||||
| 1 | Речь воспринимается легко: необоснованные паузы отсутствуют; фразовое ударение и интонационные контуры, произношение слов без нарушений нормы. Допускается не более 5 фонетических ошибок, в том числе 1–2 ошибки, искажающие смысл |
|
||||
| 0 | Речь воспринимается с трудом из-за большого количества неестественных пауз, запинок, неверной расстановки ударений и ошибок в произношении слов, ИЛИ сделано более 5 фонетических ошибок, ИЛИ сделано 3 и более фонетические ошибки, искажающие смысл |
|
||||
|
||||
---
|
||||
|
||||
## Задания 2–3 — Верно / Неверно / В тексте не сказано (2 задания, 2 балла)
|
||||
## Задание 2 — Диалог-расспрос: вопросы (4 вопроса × 0–1 балл = 4 балла)
|
||||
|
||||
Формат: ученик слышит диалог. По 2 утверждения, нужно определить:
|
||||
- 1 (True) — утверждение соответствует диалогу
|
||||
- 2 (False) — утверждение противоречит диалогу
|
||||
- 3 (Not stated) — информация в диалоге не упоминается
|
||||
Ученик задаёт 4 вопроса по заданной теме.
|
||||
|
||||
Оценивание: 1 балл за каждый верный ответ, максимум 2 балла.
|
||||
| Балл | Критерий |
|
||||
|------|----------|
|
||||
| 1 | Вопрос по содержанию отвечает поставленной задаче; имеет правильную грамматическую форму прямого вопроса; возможные фонетические и лексические погрешности не затрудняют восприятия |
|
||||
| 0 | Вопрос не задан, ИЛИ заданный вопрос по содержанию не отвечает поставленной задаче, И/ИЛИ не имеет правильной грамматической формы прямого вопроса, И/ИЛИ фонетические и лексические ошибки препятствуют коммуникации |
|
||||
|
||||
---
|
||||
|
||||
## Задания 4–9 — Выбор из трёх вариантов (6 заданий, 6 баллов)
|
||||
## Задание 3 — Диалог-интервью: ответы (5 ответов × 0–1 балл = 5 баллов)
|
||||
|
||||
Формат: ученик слышит 6 монологов/диалогов. К каждому — вопрос с 3 вариантами ответа.
|
||||
Нужно выбрать верный вариант: 1, 2 или 3.
|
||||
Ученик отвечает на 5 вопросов интервьюера.
|
||||
|
||||
Оценивание: 1 балл за каждый верный ответ, максимум 6 баллов (2 задания по 3 баллов).
|
||||
|
||||
> Примечание по структуре: в разных вариантах ФИПИ количество заданий блоков может
|
||||
> незначительно варьироваться. Агент должен ориентироваться на фактические ключи
|
||||
> проверяющего, а не на фиксированную нумерацию выше.
|
||||
| Балл | Критерий |
|
||||
|------|----------|
|
||||
| 1 | Дан полный и точный ответ на запрос информации: 2–3 коммуникативно обусловленные фразы, в которых отсутствуют элементарные лексико-грамматические и/или фонетические ошибки |
|
||||
| 0 | Ответ на вопрос не дан, ИЛИ содержание ответа не соответствует запросу информации, ИЛИ ответ содержит менее 2 фраз, ИЛИ в ответе имеются элементарные лексико-грамматические и/или фонетические ошибки (в том числе когда ответ носит характер набора слов) |
|
||||
|
||||
---
|
||||
|
||||
## Итоговая таблица
|
||||
## Задание 4 — Монологическое высказывание (0–10 баллов по 3 критериям)
|
||||
|
||||
| Блок | Задания | Тип | Макс. баллов |
|
||||
|------|---------|-----|-------------|
|
||||
| Соответствие | 1 | A–F → 1–7 | 6 |
|
||||
| True/False/Not stated | 2–3 | 1/2/3 | 2 |
|
||||
| Выбор ответа | 4–9 | 1/2/3 | 6 (иногда делится на подблоки) |
|
||||
| | | **Итого** | **12** |
|
||||
Тематическое монологическое высказывание высокого уровня с элементами описания и рассуждения.
|
||||
|
||||
### Критерий 1: Решение коммуникативной задачи / Содержание (0–4 балла)
|
||||
|
||||
| Балл | Критерий |
|
||||
|------|----------|
|
||||
| 4 | Коммуникативная задача выполнена полностью — содержание полно, точно и развёрнуто отражает все аспекты, указанные в задании (12–15 фраз) |
|
||||
| 3 | Коммуникативная задача выполнена в основном: 1 аспект не раскрыт (остальные раскрыты полно) ИЛИ 1–2 аспекта раскрыты неполно/неточно (12–15 фраз) |
|
||||
| 2 | Коммуникативная задача выполнена не полностью: 1 аспект не раскрыт и 1 раскрыт неполно/неточно ИЛИ 3 аспекта раскрыты неполно/неточно (10–11 фраз) |
|
||||
| 1 | Коммуникативная задача выполнена частично: 1 аспект не раскрыт и 2 раскрыты неполно/неточно, ИЛИ 2 аспекта не раскрыты (остальные раскрыты полно), ИЛИ все аспекты раскрыты неполно/неточно (8–9 фраз) |
|
||||
| 0 | Коммуникативная задача выполнена менее чем на 50%: 3 или более аспекта не раскрыты, ИЛИ 2 аспекта не раскрыты и 1 и более раскрыты неполно/неточно, ИЛИ 1 аспект не раскрыт и остальные раскрыты неполно/неточно, ИЛИ объём высказывания — 7 и менее фраз |
|
||||
|
||||
### Критерий 2: Организация высказывания (0–3 балла)
|
||||
|
||||
| Балл | Критерий |
|
||||
|------|----------|
|
||||
| 3 | Высказывание логично; имеет завершённый характер (есть вступительная фраза с обращением к другу И заключительная фраза); средства логической связи используются правильно. Допускается 1 ошибка в логичности/средствах логической связи |
|
||||
| 2 | Высказывание в основном логично и имеет достаточно завершённый характер (есть вступительная фраза с обращением к другу И заключительная фраза); имеются 2–3 ошибки в логичности/средствах логической связи |
|
||||
| 1 | Высказывание не имеет завершённого характера: отсутствует вступительная ИЛИ заключительная фраза, И/ИЛИ имеются 4–5 ошибок в логичности/средствах логической связи |
|
||||
| 0 | Высказывание не имеет завершённого характера: отсутствуют вступительная И заключительная фразы, И/ИЛИ имеются 6 и более ошибок в логичности/средствах логической связи |
|
||||
|
||||
### Критерий 3: Языковое оформление (0–3 балла)
|
||||
|
||||
| Балл | Критерий |
|
||||
|------|----------|
|
||||
| 3 | Словарный запас, грамматические структуры, фонетическое оформление соответствуют задаче. Допускается не более 3 негрубых лексико-грамматических ошибок И/ИЛИ не более 3 негрубых фонетических ошибок |
|
||||
| 2 | Словарный запас, грамматика, фонетика в основном соответствуют задаче. Допускается не более 4–5 лексико-грамматических (из них не более 2 грубых) И/ИЛИ не более 4–5 фонетических ошибок (из них не более 2 грубых) |
|
||||
| 1 | Языковое оформление частично соответствует задаче. Допускается не более 6–7 лексико-грамматических (из них не более 3 грубых) И/ИЛИ не более 6–7 фонетических ошибок (из них не более 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 балл)
|
||||
- Задания 2–9: совпадение цифры (ученик: 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)
|
||||
- Проверить: полнота (2–3 фразы) + соответствие вопросу + отсутствие грубых ошибок
|
||||
|
||||
**Задание 4 (монолог):**
|
||||
- К1: посчитать аспекты из задания, проверить раскрытие каждого + подсчитать фразы
|
||||
- К2: проверить наличие вступления с обращением, заключения, логических связок
|
||||
- К3: посчитать лексико-грамматические и фонетические ошибки, разделить на грубые/негрубые
|
||||
|
||||
### Формат вывода
|
||||
|
||||
Три блока с таблицами: задание | ответ ученика | ключ | результат (+ или -)
|
||||
Итог по каждому блоку + общий итог из 12.
|
||||
Список ошибок с пояснением: "Задание 1B: ученик — 5, верный ответ — 1".
|
||||
Если есть нераспознанные задания — явно указать какие и что они засчитаны как 0.
|
||||
Для каждого задания: балл + краткое обоснование со ссылкой на критерий.
|
||||
Итоговая таблица: задание | балл | макс.
|
||||
Общий итог из 20.
|
||||
При наличии ошибок — конкретные примеры из транскрипта.
|
||||
Loading…
Add table
Add a link
Reference in a new issue