Tea Taste: запись с телефона и сбой, о котором никто не сообщал
Импорт записей, сделанных вне приложения: как одна запись всей дегустации лишала разбор всех дескрипторов, почему распознавание теряет номер пролива, а не само слово, и как молчаливое слияние проливов стало видимым. Плюс линтер, который числился в проекте, но не существовал.
Предыдущий разбор был посвящён голосовым заметкам: запись в браузере, распознавание, разбор расшифровки в поля по фиксированному словарю. Он исходил из того, что запись делается в приложении. На практике дегустацию часто записывают на телефон — отдельно, без открытой формы.
Это обновление принимает такие записи. Сама загрузка оказалась простой; интересным оказалось то, что вскрылось на реальной записи.
Запись по проливу не потребовала ничего
Эндпоинт загрузки и раньше принимал m4a, mp3, wav, ogg и aac и перекодировал всё, что приходило. Поэтому импорт записи одного пролива не потребовал изменений на сервере: номер пролива определяется тем, в какой блок формы брошен файл, — ровно так же, как для записи, сделанной в приложении.
Попутно исправлено одно ограничение интерфейса. Браузер без MediaRecorder показывал только сообщение о том, что записывать звук он не умеет, — и этим скрывал импорт, которому микрофон не нужен вовсе. Запись больше не является условием для выбора файла.
Когда вся дегустация — один файл
Трудным оказался другой случай: один файл на всю дегустацию. У такой записи нет номера пролива — проливы находятся внутри неё и названы вслух.
Оставленная с brewingNumber: 0, она группировалась с общей заметкой «о чае» и помечалась для разбора как [О чае]. На реальной записи это стоило дескрипторов целиком: модель прочла всю дегустацию как одно общее замечание и не вернула ни одного аромата и ни одного вкуса.
Поэтому у сегмента появился собственный признак whole. Такая запись образует отдельную группу распознавания, помечается как [Вся сессия] и сопровождается правилом: разделить её по номерам, которые называет говорящий, и ничего из неё не переносить в описание самого чая.
if (whole) return `[Вся сессия] ${transcript}`;
Пометка выглядит мелочью, но именно она решает, как модель прочтёт весь текст.
Что теряет распознавание
На той же записи обнаружилась закономерность, которую стоит зафиксировать: распознавание теряет не слово «пролив», а номер рядом с ним. Пять проливов, названных как «N пролив», пришли целыми. Шестой, названный «в шестом проливе», вернулся как «в простом проливе» — и этот пролив слился с соседним.
Второе наблюдение касается словаря. «Немного грачит» — это «горчит», горечь: слово, которое звучит в каждой дегустации и которого SpeechKit в этом значении не знает. Оно добавлено в правило исправлений вместе с другими дегустационными словами, которые распознавание искажает.
Напряжение, которое не снимается подсказкой
Самое поучительное в обновлении — честно зафиксированный компромисс. Усиление правила разбиения восстанавливало шестой пролив, но теряло все дескрипторы. Усиление правила разбора оттенков возвращало дескрипторы, но снова сливало тот пролив.
Выбор сделан в пользу дескрипторов: в пересчёте на пролив они ценнее. А структурный ответ лежит вне подсказки модели — в единообразной формулировке. Справка теперь просит называть пролив одинаково, в начале заметки: «пролив N».
Сбой, о котором теперь сообщают
Главная проблема была даже не в слиянии, а в том, что оно происходило молча. Шестой пролив исчезал, и ничто на экране об этом не говорило.
Теперь панель предложений сравнивает число найденных проливов с числом проливов в форме и сообщает о расхождении прямо:
Нашлось {n} проливов из {total}. Проверьте, что номер каждого пролива назван вслух — ненайденный пролив молча сливается с предыдущим.
Исправить само распознавание невозможно; сделать его отказ видимым — возможно. Это разные задачи, и вторая здесь оказалась выполнимой.
Линтер, которого не было
Вторая половина обновления — о гигиене, и начинается она с находки: в API был скрипт lint, за которым не стояло линтера. npx eslint . в проекте без eslint и без конфигурации.
После установки airbnb-base автоматическое исправление закрыло кавычки, точки с запятой и пробелы, а 64 замечания пришлось разобрать вручную. Большинство оказались наведением порядка, но три — настоящими дефектами:
routes/index.js— заготовка express-generator, которую так и не удалили. Она присваиваетexpress = require('express').Router(), а затем вызываетrouter.get, поэтому её подключение бросаетReferenceError, и отрисовывает шаблон через движок, которого в приложении нет. Её никто не подключал. Удалена.NODE_ENV == 'production'в параметрах cookie обоих путей входа — рядом сsecure: NODE_ENV === 'production'на следующей строке. Для строкового окружения результат одинаков, но пара читается как намеренное различие, которым не является.formJsonLdпринималаbrewingsи не использовала их.
Шесть правил airbnb отключены, и причина каждого записана рядом с ним. Три из них о переносах: измеренно они давали бо́льшую часть 3 500-строчного диффа автоисправления, и ни одна из этих строк не стала читаемее.
Во фронтенде airbnb сознательно не применён. Исходники разделены между табуляциями и четырьмя пробелами по файлам, и переотступ всего репозитория похоронил бы каждую настоящую находку под тысячами строк пробелов. Одиннадцать предупреждений о зависимостях эффектов оставлены — это эффекты только монтирования, — но каждое теперь сопровождается строкой, объясняющей, какое именно, чтобы следующее появившееся было настоящим вопросом, а не очередным шумом.
Зависимость, работавшая случайно
uuid импортировался двумя компонентами и никогда не был объявлен в package.json. Он разрешался только потому, что npm поднимал транзитивную копию на верхний уровень node_modules. Обновление lock-файла, переставшее её поднимать, сломало бы сборку без единого собственного изменения. Теперь зависимость объявлена.
Пять пакетов, которые никто не импортировал, удалены. Размер сборки при этом не изменился — 297,94 kB в gzip: неимпортируемый пакет в неё и не попадал. Выигрыш в другом — время установки, размер lock-файла и поверхность цепочки поставок.
В обоих репозиториях линтер теперь запускается в CI перед тестами: он быстрее, а ошибка линтера обычно оказывается опечаткой, которую тесты нашли бы за три минуты.
Выводы
- Сделать отказ видимым часто проще, чем устранить его причину. Слияние проливов распознаванием не исправить, но о нём можно сообщить.
- Пометка входных данных определяет, как модель их прочтёт. Одна и та же запись под
[О чае]и под[Вся сессия]даёт противоположный результат. - Распознавание теряет в первую очередь то, что звучит неоднородно. Единообразная формулировка надёжнее усложнения подсказки.
- Скрипт, за которым ничего не стоит, опаснее отсутствия скрипта: он создаёт уверенность, что проверка есть.
- Работающее случайно не работает: зависимость, разрешавшаяся подъёмом транзитивной копии, держалась на устройстве чужого инструмента.
Доступность
Сервис открыт: teaform.ru. Импорт записи доступен в каждом блоке пролива, импорт всей дегустации — отдельной страницей в разделе «Дегустация».
- Исходный код: github.com/nikita-konkin/tea-taste-frontend
Теги
Другие обновления по проекту
Tea Taste: дегустация голосом — расшифровка речи и словарь, который её удерживает
Голосовые заметки вместо заполнения формы: распознавание речи через SpeechKit, разбор расшифровки в поля по фиксированному словарю дескрипторов, месячная квота, фотографии дегус...
Tea Taste: восемь скрытых отказов бэкенда, которые нашло код-ревью
Разбор восьми стабильностных дефектов в API Tea Taste, найденных при код-ревью после июльского релиза: двойной HTTP-сервер, ошибки, которые молча проваливались мимо обработчиков...