Задача: Добавить браузерное распознавание отдельных слов по ответу ребёнка
Добавить браузерное распознавание отдельных слов по ответу ребёнка
Распознавать короткий голосовой ответ ребёнка через Web Speech API в заданиях вида «Что изображено на картинке?».
Цель
Добавить голосовой ответ ребёнка в упражнениях вида «Что изображено на картинке?». Браузер распознаёт короткое слово, приложение сравнивает распознанный текст с ожидаемым ответом.
Пример:
- На экране изображена собака.
- Ребёнок нажимает кнопку микрофона.
- Говорит: «Собака».
- Браузер возвращает текстовую гипотезу.
- Приложение сравнивает её с ожидаемым словом.
- Показывает результат и позволяет повторить попытку.
На первом этапе используем встроенный Web Speech API, без LLM и без собственного фонетического анализа.
Основное API
В разных браузерах класс может называться по-разному:
const SpeechRecognitionClass =
window.SpeechRecognition ||
window.webkitSpeechRecognition
const recognition = new SpeechRecognitionClass()
Базовая настройка:
recognition.lang = 'ru-RU'
recognition.continuous = false
recognition.interimResults = false
recognition.maxAlternatives = 5
Основные параметры:
lang = 'ru-RU'— русский язык;continuous = false— один короткий ответ, затем завершение;interimResults = false— использовать только финальный результат;maxAlternatives = 5— запросить несколько вариантов распознавания, если браузер их предоставляет.
Получение результата
recognition.onresult = event => {
const result = event.results[event.results.length - 1]
const alternatives = Array.from(result).map(item => ({
transcript: item.transcript,
confidence: item.confidence,
}))
console.log(alternatives)
}
Пример результата:
[
{ transcript: 'собака', confidence: 0.91 },
{ transcript: 'сабака', confidence: 0.62 },
{ transcript: 'собачка', confidence: 0.41 },
]
confidence не гарантированно одинаково полезен во всех браузерах, поэтому основная логика должна опираться на текстовые гипотезы, а confidence использовать как дополнительный сигнал.
Нормализация текста
Перед сравнением нужно привести строки к единому виду:
export const normalizeRecognizedText = (value: string) =>
value
.toLowerCase()
.trim()
.replace(/ё/g, 'е')
.replace(/[^а-яa-z0-9\s-]/gi, '')
.replace(/\s+/g, ' ')
Пример:
normalizeRecognizedText(' Собака! ')
// 'собака'
Точное и приблизительное сравнение
Сначала проверяем точное совпадение:
const exact =
normalizeRecognizedText(recognized) ===
normalizeRecognizedText(expected)
Если точного совпадения нет, используем строковую близость. На первом этапе достаточно Levenshtein distance.
Levenshtein
export const levenshteinDistance = (
a: string,
b: string,
): number => {
const rows = a.length + 1
const cols = b.length + 1
const matrix = Array.from(
{ length: rows },
() => Array<number>(cols).fill(0),
)
for (let i = 0; i < rows; i++) {
matrix[i][0] = i
}
for (let j = 0; j < cols; j++) {
matrix[0][j] = j
}
for (let i = 1; i < rows; i++) {
for (let j = 1; j < cols; j++) {
const cost = a[i - 1] === b[j - 1] ? 0 : 1
matrix[i][j] = Math.min(
matrix[i - 1][j] + 1,
matrix[i][j - 1] + 1,
matrix[i - 1][j - 1] + cost,
)
}
}
return matrix[a.length][b.length]
}
Нормализованный score:
export const wordSimilarity = (
a: string,
b: string,
): number => {
const left = normalizeRecognizedText(a)
const right = normalizeRecognizedText(b)
const maxLength = Math.max(left.length, right.length)
if (maxLength === 0) {
return 1
}
return 1 - levenshteinDistance(left, right) / maxLength
}
Пример:
wordSimilarity('собака', 'сабака')
// ~0.83
Важно: это только строковое сравнение. Никакого собственного распознавания фонем здесь не делать.
Выбор лучшей гипотезы
type RecognitionAlternative = {
transcript: string
confidence: number
}
export const findBestAlternative = (
expected: string,
alternatives: RecognitionAlternative[],
) => {
return alternatives
.map(item => ({
...item,
normalized: normalizeRecognizedText(item.transcript),
similarity: wordSimilarity(expected, item.transcript),
}))
.sort((a, b) => b.similarity - a.similarity)[0] ?? null
}
Использование:
const best = findBestAlternative('собака', alternatives)
if (best?.similarity === 1) {
// точное совпадение
} else if (best && best.similarity >= 0.8) {
// вероятно правильный ответ с небольшой ошибкой распознавания
} else {
// ответ отличается заметно
}
Порог 0.8 здесь не считать истиной. Его нужно подобрать по реальным детским ответам.
Полный минимальный helper распознавания
export const recognizeSingleWord = () =>
new Promise<RecognitionAlternative[]>((resolve, reject) => {
const SpeechRecognitionClass =
window.SpeechRecognition ||
window.webkitSpeechRecognition
if (!SpeechRecognitionClass) {
reject(new Error('SpeechRecognition is not supported'))
return
}
const recognition = new SpeechRecognitionClass()
recognition.lang = 'ru-RU'
recognition.continuous = false
recognition.interimResults = false
recognition.maxAlternatives = 5
recognition.onresult = event => {
const result = event.results[event.results.length - 1]
const alternatives = Array.from(result).map(item => ({
transcript: item.transcript,
confidence: item.confidence,
}))
resolve(alternatives)
}
recognition.onerror = event => {
reject(new Error(event.error))
}
recognition.onnomatch = () => {
resolve([])
}
recognition.start()
})
Состояния UI
Минимальная state machine:
type RecognitionState =
| 'idle'
| 'listening'
| 'processing'
| 'correct'
| 'almost-correct'
| 'incorrect'
| 'no-match'
| 'error'
Пример логики:
setState('listening')
try {
const alternatives = await recognizeSingleWord()
setState('processing')
const best = findBestAlternative(expectedWord, alternatives)
if (!best) {
setState('no-match')
} else if (best.similarity === 1) {
setState('correct')
} else if (best.similarity >= threshold) {
setState('almost-correct')
} else {
setState('incorrect')
}
} catch {
setState('error')
}
Важные ограничения
SpeechRecognitionподдерживается браузерами неравномерно;- в некоторых реализациях распознавание может выполняться внешним сервисом браузера/ОС;
- доступ к микрофону требует разрешения пользователя;
maxAlternativesне гарантирует, что браузер всегда вернёт ровно указанное количество вариантов;confidenceзависит от реализации и не должен быть единственным критерием;- маленькие слова особенно чувствительны к одной ошибке, поэтому threshold должен учитывать длину слова;
- не пытаться поверх этого строить собственное распознавание фонем: используем готовый ASR и сравниваем только его текстовые гипотезы.
Критерий готовности
Для задания с картинкой приложение умеет по нажатию кнопки принять одно короткое русское слово, получить несколько текстовых гипотез, нормализовать их, выбрать наиболее близкую к ожидаемому слову и вернуть понятный результат: точное совпадение / близкий ответ / другой ответ / не распознано / ошибка.