Предыстория
Никогда не был любителем голосового интерфейса, но пробовал дома и Amazon Echo, и Алису. Все-таки очень долго это и недостаточно надежно - произносить фразу и думать потом - правильно ли меня поняли и всё ли сделано, как я хотел.
Но после прочтения статьи и, главное, обсуждений после нее я пришел к выводу, что есть варианты, когда это правда удобно. Собственно, самым ярким мне показался пример с кухонным таймером - не хочется грязными руками что-то трогать - голосовой интерфейс тут идеален. А попробовав приложение и почитав код коллеги @janvarev я понял, что современные средства распознавания уже вышли на очень приличный уровень и легко подключаются в проекты с открытым кодом. Дальше стало интересно сделать что-то более удобное и более стабильно работающее (без обид, но проект "Ирина" у меня не весь заработал при вменяемых затратах времени и настроек там меньше, чем мне хотелось бы).
Задумки
Целевой аудиторией приложения я вижу в первую очередь обывателя или продвинутого пользователя, знакомого с элементарной настройкой приложения через текстовый файл или GUI. Я хорошенько обдумал с точки зрения конечного пользователя, что бы мне хотелось видеть в приложении для домашнего пользования. В первую очередь рассматривал проект с точки зрения обывателя, которого считаю основной целевой аудиторией подобных проектов. Получился довольно небольшой список хотелок:
Простота установки и настройки - это либо инсталлятор и GUI для настройки, либо портативное приложение с настройками в простых текстовых или json/xml файлах (простите, но yaml я считаю крайне неудобным и опасным форматом для ручного редактирования, а старый добрый .ini не очень технологичен).
Работа с явно указанными аудио устройствами - я, например, по работе использую гарнитуру, но музыку вывожу на колонки. И вовсе не очевидно, какое устройство я хочу отдать голосовому помощнику, а какое у меня задействовано для других целей. И постоянно переключаться я совершенно не хочу. А для голосового помощника вполне разумным было бы использовать отдельный внешний выносной микрофон, в идеале, с кнопкой выключения микрофона. Например, мне понравилось качество работы и исполнения здесь.
Расширение функционала плагинами - тут всё очевидно - я лично не смогу (даже если захочу) обеспечить все хотелки пользователей. Например, такая система должна легко встраиваться в "Умный дом" и кому-то придется это писать, а системы разные...
Возможность гибкой настройки не только приложения, но и плагинов - в частности, смена команд и ключевой фразы, а так же настройки безопасности - что могут получать плагины из ядра (звуковой поток, поток распознанных слов, и т.п.).
Максимальная автономность приложения - никаких распознаваний или генерации речи в облаке - в облаках уже есть готовые прекрасные решения, с которыми невозможно конкурировать.
Невысокие требования к ресурсам - в силу ограниченности бюджета простого пользователя хотелось бы уместить все в общепринятые объемы памяти, дискового пространства и физические габариты, пригодные для установки в бытовых помещениях. Т.е. более-менее любая машина, которая тянет Windows10 (к сожалению, стандарт де-факто) уровня Celeron/Atom с 4 Gb и 64 Gb диска должна подходить. Приятно, что такие машины бывают довольно компактными (Intel NUC, Gigabyte BRIX, множество китайских машин на базе Intel BayTrail Atom Z35xx/83xx/85xx) - их можно подключить к телевизору в качестве медиаплеера и, заодно, использовать в качестве голосового помощника.
Возможность несложной смены языка общения - русский язык, конечно, мне близок, но хотелось бы принести пользу максимально широкому кругу лиц. И тут надо понимать, что это 3 направления работы - язык распознавания, язык генерации речи и язык команд /плагинов.
Кросс-платформенность - хорошо бы иметь возможность запускать помощника на разных системах, хотя, конечно, Windows более распространен в широких кругах...
Конечно же, все эти благие намерения необязательно получится реализовать все и сразу.
Реализация
Для реализации я выбрал язык C#, как наиболее подходящий из знакомых мне. Фреймворк - устоявшийся .NET Core 3.1 LTS.
Основные модули, на которых основывается проект:
Модуль распознавания - VOSK
Генерация речи - Windows SAPI5 с голосами
Захват и вывод аудио - NAudio
Мягкое сравнение строк - FuzzySharp
Проект выложен на GitHub и понемногу развивается моими силами.
Идея была такова - основной модуль занимается захватом аудио и распознаванием речи, а так же поиском команд, которые определены плагинами. Плагины, в свою очередь, максимально автономны и от ядра получают только пинок "на старт", аудиопоток, а так же поток распознанных слов (если пользователь в настройках разрешил).
Учитывая несовершенство технологии распознавания, поиск команд должен использовать "мягкое сравнение", чтобы не отказывать при, например, изменении окончания в слове. Конечно, это может породить ложные срабатывания, поэтому важно экспериментальным путем подобрать коэффициенты совпадения по каждому слову в команде. На качество распознавания, очевидно, будет влиять дистанция до микрофона, качество микрофона, особенности произношения пользователя и модель распознавания, поэтому итоговый результат сложно предсказать - коэффициенты лучше подбирать индивидуально.
С точки зрения распознавания команд я решил упростить себе жизнь и не пытаться реализовать какие-либо подобия искусственного интеллекта или нейронных сетей - решил просто искать фразы по шаблонам, но делать это по-возможности гибко, плюс дать возможность пользователю возможность самому наращивать базу команд.
На реализацию механизма ядра ушла примерно неделя вечерами - сделать, переделать, попробовать, сделать новый плагин, добавить в интерфейс еще возможностей...
Собственно, сама разработка была интересна, в первую очередь, архитектурой взаимодействия между ядром и плагинами, а так же механизмом вычленения команд из потока распознанных слов - тут пришлось подумать. Остальное было простым складыванием кубиков. Поэтому я не планирую тут публиковать каких-то кусков кода - прорывных алгоритмов там нет, а основную идею алгоритма я предпочту описать на словах.
Итак, сложность работы с командами заключается в том, что команд много, они набираются из разных плагинов, могут пересекаться, а еще они могут содержать параметры - это неизвестные мне слова или сочетания слов (что важно!). Так же, распознанные слова поступают потоком, без явно выраженных границ фраз. Тут, конечно, очень помогает то, что ориентиром начала команды является кодовое слово (имя помощника). Похожие проблемы я уже решал при написании парсеров протоколов ESC/POS, ССTalk и протокола фискального регистратора, поэтому подход применил знакомый - при получении кодового слова я формирую массив, в котором есть все возможные команды, объявленные в плагинах, и далее выкидываю из массива те команды, которые не содержат очередного пришедшего из потока слова.
С параметрами чуть сложнее - они могут быть многословными, поэтому в очередной параметр набиваются все слова, которые не соответствуют следующему за параметром слову команды. Таким образом я понемногу выстраиваю из полученных слов команду до тех пор, пока в массиве не останется только одна команда или, если в конце команды идет параметр, не пройдет таймаут ожидания следующего слова. Тут у меня возникла дилемма - если в конце команды будет стоять параметр, то в него будут набито только одно слово - такова специфика потока распознавания - слова выдаются пачками, когда пользователь делает длинную паузу во фразе и я не смог найти признаков, по которым еще можно было бы оценить конец фразы. Это, безусловно, задачка на "подумать" в ближайшее время, но пока я просто предполагаю, что в таком случае параметр должен быть однословным, а так же рекомендую пользователю не ставить параметры в конце фразы. Наверняка, в большинстве случаев, можно построить фразу так, чтобы было какое-то завершающее слово.
С учетом описанной выше особенности потока распознанных слов, я не могу использовать группировку слов в приходящих пакетах - не могу считать окончание пакета окончанием фразы, так же как слова в одном пакете относящимися к одной фразе - настройка длительности паузы, после которой формируется пакет мне недоступна. В связи с этим я ввел 2 таймера:
первый таймер устанавливает задержку по времени, в течение которой после прихода ключевого слова должны подойти слова команды. Это длительная задержка, во время которой пользователь обдумывает (возможно, вспоминает формат) команду, которую он хочет произнести. Я же в это время приглушаю звук на текущем основном устройстве, чтобы играющая музыка или воспроизводящийся фильм не вносил шумы в аудиоканал. После завершения ввода команды громкость звука восстанавливается. Собственно, это подсмотрено у Алисы. :) Пауза помогает мне не зависнуть, если команды так и не будет произнесено.
второй таймер определяет длительность паузы между словами. Просто чтобы не застрять на ожидании команды похожей на найденную, но более длинной. Ну и не зависнуть на неоконченной команде, конечно.
Если любой из таймеров сработал и никакой команды не найдено, то я звуковым сигналом отмечаю конец ожидания. Вот этот момент мне очень не нравится в существующих голосовых помощниках - никак нельзя понять, когда контекст ввода команды прервался - возможно, пользователь просто подбирал слова для продолжения команды и слишком задержался - он будет продолжать договаривать не зная, что это уже бесполезно и полученный результат не будет соответствовать его ожиданиям.
По выполнению команды описывать особенно нечего - это асинхронный процесс и, собственно, его логика полностью определяется автором плагина. Плагин может управлять флажком разрешения на передачу ему данных аудиопотока и потока распознанных слов - например, это может понадобиться плагину, реализующему передачу звука на другие устройства - вариант голосового оповещения. Так же можно организовать голосовую связь или запись голосовых сообщений. Тогда нужна вторая команда, которую распознает ядро и передаст плагину сигнал на прекращение захвата аудио, или сам плагин может отслеживать поток распознанных слов на наличие "стоп-слова".
Плагины
На текущий момент я сделал несколько тестовых несложных плагинов. Все они используют модель конфигурирования в том виде, в каком я его задумал - доступное пользователю описание команд и параметров в JSON-файле. Разработчик не обязательно должен придерживаться такого формата. Ядро от плагина хочет получить лишь список команд, которые плагин умеет выполнять. Но даже тут разработчик может отдать пустой список, а потом подключиться к потоку распознанных слов (в-то и вообще к аудио потоку) и искать свои команды сам. Но это если пользователь в настройках плагинам подслушивать. Но, например, так можно реализовать срабатывание на громкий звук. :)
Итак, плагины:
AppControlPlugin - позволяет по команде имитировать нажатие кнопки в заданной программе. Например, так можно управлять видеоплеером - по команде "Останови фильм" в процесс "mpc-H64" будет передаваться нажатие пробела и плеер будет вставать на паузу.
BrowserPlugin - позволяет открывать заранее заданные ссылки в браузере (в стандартном или в новом окне). Например, по команде "Открой пробки" будет открывать в браузере ссылку "https://yandex.ru/maps". Если в настройках указан запуск в отдельном окне, то плагин запоминает ссылку на процесс и позже может закрыть окно по команде "Закрой пробки".
CurrencyRatePlugin - может озвучивать текущий курс валюты (берет с сайта ЦБ).
HelloPlugin - просто отвечает на приветствие - варианты команд-приветствий и ответов можно задать самому. Делался, скорее, для тестов. :)
RunProgramPlugin - по команде запускает заданную в настройках программу. Например, по "Запусти блокнот" запустит "notepad.exe". Тоже умеет запоминать процессы и закрывать по отдельной просьбе.
TimerPlugin - запускает таймер на то кол-во минут/секунд, которое вы назовете. Удобно для кухни. :) Опять-таки, умеет запущенные таймеры останавливать.
Установка
Тут все просто - модуль ядра со всем необходимым распаковывается из архива и программа готова к работе. В случае порчи конфигов что у ядра, что у плагинов - можно просто стереть конфиги и программа перегенерит их со значениями по-умолчанию.
Плагины должны лежать в папке, которая указана в настройках. При этом неважно, в какой форме - можно их набросать в корень папки или сделать для каждого плагина отдельную папку. Важно лишь, чтобы имя файла плагина совпадало с маской, заданной в настройках. Ядро ищет по маске во всех вложенных папках и каждому плагину передается его путь, чтобы он мог подтягивать ресурсы (конфигурацию, звуковые файлы) из своей папки.
Модели распознавания берутся с сайта VOSK и кладутся в папку, указанную в настройках.
Модели для синтеза речи используются из реестра Windows - можно установить штатные от Microsoft, а можно добавить варианты от RHVoice Lab.
Настройка
К сожалению, пока у меня не было времени на написание отдельной GUI утилиты настройки, но при желании, это можно будет сделать. Пока что все настройки делаются в JSON-файлах - отдельных для ядра и каждого плагина.
Опишу самые важные настройки ядра:
"ModelFolder": "model" - папка для модели распознавания речи.
"SelectedAudioInDevice": "" - устройство захвата звука. Если пусто или не найдено, то используется устройство по-умолчанию Windows. Название сравнивается как .StartsWith(), поэтому полная строка не обязательно - лишь бы не совпала с чем-то еще. Список доступных выводится в консоль при старте программы.
"SelectedAudioOutDevice": "" - устройство вывода звука. Если пусто или не найдено, то используется устройство по-умолчанию Windows. Название сравнивается как .StartsWith(), поэтому полная строка не обязательно - лишь бы не совпала с чем-то другим из списка. Список доступных выводится в консоль при старте программы.
"CallSign": [ "Вася" ] - набор ключевых слов или имен помощника. Ищется любое из них. Можно перечислить несколько вариантов произношения для повышения вероятности распознавания. На коротких словах, к сожалению, мягкое распознавание малоэффективно - слишком велик вес каждого отклонения.
"DefaultSuccessRate": 90 - коэффициент совпадения сстроки. Используется пока только для поиска ключевого слова.
"VoiceName": "Aleksandr" - название модуля генерации речи. Если пусто или не найдено, то используется первый в списке с подходящей культурой. Список доступных выводится в консоль при старте программы.
"SpeakerCulture": "ru-RU" - указание на язык пользователя. Используется при выборе генератора речи и передается в плагины, чтобы они знали язык пользователя. Например, таймер использует этот параметр для выбора конвертора из слов в цифры и обратно.
"PluginsFolder": "plugins" - имя папки для плагинов
"PluginFileMask": "*Plugin.dll" - маска файлов плагинов.
"StartSound": "AssistantStart.wav" - звуковой файл, который проигрывается при старте помощника.
"MisrecognitionSound": "Misrecognition.wav" - звуковой файл, который проигрывается при ошибке распознания команды.
"CommandAwaitTime": 10 - время ожидания команды после получения ключевого слова.
"NextWordAwaitTime": 3 - время ожидания следующего слова, если ввод команды уже начат.
"CommandNotRecognizedMessage": "Команда не распознана" - фраза, которую ядро произносит в случае ненайденной команды. Тут можно вставить фразу на более понятном пользователю языке.
"CommandNotFoundMessage": "Команда не найдена" - фраза, которую ядро произносит в случае ненайденной команды. Тут можно вставить фразу на более понятном пользователю языке.
"AllowPluginsToListenToSound": false - разрешить передавать плагинам звуковой поток с микрофона.
"AllowPluginsToListenToWords": false - разрешить передавать плагинам поток распознанных слов.
Предполагается, что плагины будут использовать похожий интерфейс загрузки настроек, но это не обязательно - автор плагина должен лишь передать ядру список команд, которые ядро будет отслеживать, но в теории, плагин может подключиться к потоку распознанных слов и ловить свои команды сам. Конечно, если пользователь в настройках ядра не запретил передачу сырых данных плагинам.
Вот фрагмент настройки плагина тамера, как наиболее показательный:
{
"AlarmSound": "timer.wav", - файл, который роигрывается при срабатывании таймера
"IncorrectTime": "Некорректное время", - фраза, которая произносится при нераспознанном времени
"TimerNotFound": "Таймер не найден", - фраза, котрая произносится при попутке удаления несуществующего таймера, ко
"Commands": [ - тут начинается список команд
Первая команда - очень простая
{
"Response": "Таймер заведен на {1} минут", - это фраза, которую плагин произносит
в результате успешного выполнения команды.
"{1}" тут означает место, в которое будет вставлено значение параметра. К сожалению, пока я не успел реализовать
свой алгормтм форматирования, чтобы тут можно было использовать более понятные метки.
Например {%minutes%} было бы удобнее и гибче.
"isStopCommand": false, - мне удобно было сделать отдельный флаг, разделяющий команды на установку и удаление таймеров
"Name": "Run timer minutes", - это просто имя или описание команды. При разборе ни на что не влияет
"Tokens": [ - это список слов,из которых состоит команда
Первое слово
{
"Value": [ - список значений слова команды. Ищется любое из указанных
"Поставь",
"Заведи",
"Запусти"
],
"Type": "Command", - тип слова - команда или параметр
"SuccessRate": 90 - коэффициент совпадения
},
Второе слово
{
"Value": [
"таймер"
],
"Type": "Command",
"SuccessRate": 90
},
Третье слово
{
"Value": [
"на"
],
"Type": "Command",
"SuccessRate": 90
},
Четвертое слово - наконец то дошли до параметра
{
"Value": [
"%minutes%" - в моем плагине по этому значению ищутся места в произнесенной фразе, где содержится параметр.
Можно было жестко апописать номера позиций, но так появляется возможность перестраивать фразу.
],
"Type": "Parameter",
"SuccessRate": 90
},
Пятое слово
{
"Value": [
"минут",
"минуты",
"минуту"
],
"Type": "Command",
"SuccessRate": 90
}
]
},
Вторая команда команда - почти копия первой, но останавливает, а не запускает таймер
{
"Response": "Таймер на {1} минут остановлен",
"isStopCommand": true,
"Name": "Stop timer minutes",
"Tokens": [
{
"Value": [
"Останови",
"Удали"
],
"Type": "Command",
"SuccessRate": 90
},
{
"Value": [
"таймер"
],
"Type": "Command",
"SuccessRate": 90
},
{
"Value": [
"на"
],
"Type": "Command",
"SuccessRate": 90
},
{
"Value": [
"%minutes%"
],
"Type": "Parameter",
"SuccessRate": 90
},
{
"Value": [
"минут",
"минуты",
"минуту"
],
"Type": "Command",
"SuccessRate": 90
}
]
},
]
}
Если следовать моему шаблону проектрования плагинов, то они получаются довольно гибкими - пользователь сам может в JSON добавить неограниченное количество своих команд, базируясь на правилах, определенных автором плагина. Например, комбинировать часы и минуты по-разному, переписать команды на другом языке. Особенно это актуально для плагинов широкого применения - например, для запуска программ или сайтов по команде или для управления запущенными программами путем имитации нажатия клавиш.
Дальнейшие планы
Далее я планирую развивать плагины - написать взаимодействие с календарем, погодой, расписаниями электричек и звуковые оповещения, а так же звуковую связь. Это должно быть интересно. :)
Так же, хочется подумать над кросс-платформенностью - сейчас это не возможно, в первую очередь, из-за NAudio, но, возможно, что-то получится придумать.
Предлагаю всем заинтересовавшимся подключаться к разработке новых плагинов и доработке ядра. Буду рад предложениям и критике, а так же любой помощи.
Комментарии (8)
blood_develop
28.03.2022 18:15Я тоже очень долго пытался сесть написать что-то подобное, но с этой работой времени все меньше и меньше. Но кто-то думает, а кто-то - делает, молодец
Успехов в развитии проекта!
Единственного, чего, на мой взгляд, не хватает - раскрытие диалога с пользователем, например, для уточнения параметров команд. Благодаря такой фиче можно было бы реализовать более сложные конструкции и даже деревья выполнения командjekyll2017 Автор
28.03.2022 19:16+1Я как-то и не думал о диалоге - это же не ИИ - с ним не поговоришь в свободных формулировках, а при жестких - каждое новое слово в команде увеличивает шанс неправильного распознавания.
В принципе, при текущей архитектуре разработчик плагина может подключиться к потоку распознанных слов и, задав вопрос, искать в потоке правильный ответ. :)
И, кстати, может сильно усложниться формат описания новых команд - для простого потребителя это может стать проблемой при добавлении своих команд.janvarev
28.03.2022 20:02Если интересно, посмотрите последнюю версию Ирины. Я там архитектурно добавил возможность использовать контекст — т.е. ситуацию, когда плагин переключает систему в состояние «слушаем не с Ирины, и распознаем команду», а «слушаем всё и передаем плагину». Может, будет полезно. Есть пример реализации в формате игры «Больше-меньше».
jekyll2017 Автор
29.03.2022 11:14Если я правильно Вас понимаю, то у меня изначально такое есть - любой плагин может поднять флаг готовности к получению аудио потока или потока распознанных слов и использовать эти данные по своему усмотрению - например, для запроса подтверждения. Я на HelloPlugin это проверял - вполне работает. Только, конечно, задержка с выдачей пакетов распознанных слов в VOSK немного портит картинку - данные получаешь только после паузы в разговоре.
Надо будет сделать рабочий тестовый вариант такого взаимодействия в HelloPlugin.
Но делать диалоговые команды я все-таки пока не хочу - повертел в голове это и, мне кажется, структуры описания диалогов в JSON получатся довольно сложные, а обрабатывать их все равно придется разработчику в самом плагине - ядро не поможет с бизнес-логикой. А нужно это редко...
Проще уж сделать 2 команды - одну изначальную, на прием пприказа и вторую - ответ на запрос по первой команде. А плагин должен будет запоминать, ждет ли он ответа...janvarev
29.03.2022 11:23Да, похоже. Да, это все равно, конечно, на плагин ложится и нужно достаточно редко, но приятно, когда такую возможность можно сделать.
Например, у меня в планах:
— если не распознано после «таймер», уточнить — «что вы сказали после таймер?»
— для электрички — «хотите поставить будильник за пять, десять, двадцать минут до электрички?». Т.е. такие допы.
У меня еще контекст сбрасывается после 10 секунд, если нет ввода — это позволяет не залипать в плагине, если вдруг пользователь передумал.
Ещё можно для ребенка арифметические задачки. Там тоже удобнее, если прямой ввод доступен в какой-то момент.
janvarev
Спасибо за развитие проекта!
Было интересно посмотреть код; обратил внимание, что вы использовали некоторые схожие архитектурные решения. Приятно, значит, я неплохо продумал архитектуру )
У вас, конечно, значительно более сложная обработка аудио + еще замах на многоязычность и GUI. Fuzzy-сравнение строк — тоже хорошая идея, я о ней думал, но у меня руки не дошли нормально ей заняться. Ну, и С#, конечно, не Python — жаль, можно было бы копировать элементы кода.
Я сейчас несколько доработал Ирину, в частности, получилось нормально сделать мультимикрофонную инсталляцию в формате «Ирина — сервер-API» и два клиента-распознавателя, у которых можно указывать аудиоустройство (разобрался как). Ну, и еще один клиент, который взаимодействует с Телеграм. Планирую написать как-нибудь на Хабре о доработках.
(Автор Ирины)
jekyll2017 Автор
Вам спасибо за идею, примеры кейсов и рассказ о возможностях и доступности современных бесплатных движков распознавания - если бы не это, никогда бы не осознал полезность голосового интерфейса.
К сожалению, как раз обработки аудио у меня нет. :( А, возможно, стоило бы - я поработал с разными микрофонами (гарнитура Jabra Evolve 20, китайский выносной микрофон на которыя я давал ссылку, микрофон ноутбука (Dell Vostro) и настольный Plantronics P610) и уже есть понимание, что качество распознавания заметно отличается. Гарнитура, конечно, лучше всего. А все недорогие варианты выносных микрофонов хорошо работают в пределах 1-1.5 метра. Буду еще искать микрофонные массивы, но пока ничего за вменяемые (для простого обывателя) деньги не нашел. :( Возможно, как раз нормализация звука помогла бы... Возможно, даже профилирование.
Многоязычность, вроде как, вполне неплохо получилось сделать - только надо подумать, как прилеплять конверторы чисел из слов в цифры - я пока сделал хардкодный вариант для русского и английского, но планирую вынести строковые константы в json-файл для возможности расширения другими языками с похожей схемой словобразования числительных. А с непохожими языками придется авторам плагинов как-то самим решать проблему. Возможно, просто распространять код.
Насчет обмена кодом - есть транслятор кода pytoc - я им как-то раз пользовался - вполне вменяемо.
Я сам не считаю свой вариант мягкого сравнения окончательным - возможно, попробую еще варианты алгоритмов. Недавно как раз почитал статью, из которой можно подчерпнуть интересные вещи. Возможно, получится составить пары звуков/букв, близких не физически, как на клавиатуре, а фонетически - это может улучшить распознавание строк.
Ну и надо будет в статью добавить описание существующих плагинов и примерной схемы их реализации для желающих поучаствовать.
janvarev
Насчет обработки аудио — я как минимум имел ввиду уменьшение громкости звука на время распознавания, и продолженное распознавание. Я-то делал по-минимуму «на коленке», меня устраивает качество даже в текущем варианте, и мне не хотелось усложнять плагинную систему.
Насчет многоязычности чисел — можете посмотреть реализацию github.com/MycroftAI/lingua-franca — это многоязычность для свободного голосового помощника MyCroft, правда, тоже на Питон. Если что, я думал её прикрутить к себе, но все-таки у меня акцент на русский вариант, не хочу распыляться.