Делаем Cloud IVR с интеллектуальной переадресацией и распознаванием за несколько минут

  • Tutorial
Стандартный сценарий, который нужно реализовывать многим бизнесам — IVR-меню при входящем звонке, которое позволяет или получить какую-то информацию или связаться с конкретным сотрудником или оператором компании. Звонящий может управлять меню либо нажимая кнопки на телефоне (DTMF), или даже голосом (ASR). Так как платформа VoxImplant позволяет быстро писать и отлаживать сценарии обработки вызовов на Javascript, то мы решили рассказать как можно за несколько минут улучшить восприятие вашего бизнеса клиентами, сделав удобное и технологичное IVR-меню. К тому же, вы сможете грамотно распределять нагрузку на вашу телефонную систему и сотрудников. За деталями, как обычно, добро пожаловать под кат.
Как обычно, нам потребуется бесплатный аккаунт разработчика VoxImplant (можно взять здесь) и немного свободного времени. Итак, чтобы продемонстрировать побольше возможностей VoxImplant, сделаем IVR, который по звонку просит назвать город России и рассказывает прогноз погоды для выбранного города. Для получения информации о погоде воспользуемся сервисом weather.yandex.ru. Итак, заходим в панель управления VoxImplant и в разделе «Приложения» создаем новое приложение, можно назвать его ivr. Внутри него идем в раздел «Сценарии»и создаем новый сценарий следующего вида:

// Подклчаем модуль распознавания речи
require(Modules.ASR);

var call,
	asr,
	city, 
	cityId,
	Weather = {}, // Yandex возвращает Weather.cities 
	tts_voice = Language.RU_RUSSIAN_FEMALE; // голос для text-to-speech

// Получение названий городов
function getCities() {
	var opts = new Net.HttpRequestOptions();
	opts.rawOutput = true;
	Net.httpRequest("http://weather.yandex.ru/static/cities.json", function (e) {
		if (e.code == 200) {

			// json возвращается как octet-stream
			var response = bytes2str(e.data, 'utf-8');
                        // получаем переменную Weather.cities
			eval(response); 

		} else Logger.write("Couldn't load cities. Status: " + e.code);
	}, opts);
}

// Ищем id города для получения данных о погоде из веб-сервиса
function getCityId(name) {
	for (var i in Weather.cities) {
		var country = Weather.cities[i];
		for (var k in country) {
			if (k.toLowerCase() == name.toLowerCase()) return country[k];
		}
	}
	return -1; // не нашли
}

// Для тестирования сценария без номера (исходящий звонок), не забудьте закомментировать, если будете делать обработку входящих, этот эвент срабатывает при каждом запуске сессии
VoxEngine.addEventListener(AppEvents.Started, function (e) {
	getCities();
	call = VoxEngine.callPSTN("ваш номер телефона", "арендованный или верифицированный номер телефона"); // вставьте ваш номер, на который придет звонок, и арендованный в Voximplant или верифицированный через панель управления номер, который отобразится как caller id
	call.addEventListener(CallEvents.Connected, handleCallConnected); // обработчик успешного соединения
	call.addEventListener(CallEvents.Failed, cleanup);
	call.addEventListener(CallEvents.Disconnected, cleanup);
});

// Обработчик входящего вызова (нужно купить номер и подключить к приложению)
VoxEngine.addEventListener(AppEvents.CallAlerting, function (e) {
	getCities();
	call = e.call;
	call.answer(); // отвечаем на звонок
	call.addEventListener(CallEvents.Connected, handleCallConnected); // обработчик успешного соединения
	call.addEventListener(CallEvents.Disconnected, cleanup);
});

// Звонок соединен
function handleCallConnected(e) {
	call.say("Здравствуйте! Назовите город России, в котором вы хотите узнать погоду.", tts_voice);
	enableASR();
}

// Включаем распознавание
function enableASR() {
  // Создаем инстанс ASR, русский язык, режим -  варианты слов из указанного словаря (названия городов)
	asr = VoxEngine.createASR(ASRLanguage.RUSSIAN_RU, ASRDictionary.ADDRESS_RU);
	// Останавливаем проигрывание, если пошел процесс захвата звука для распознавания
	asr.addEventListener(ASREvents.CaptureStarted, function (e) {
		call.stopPlayback();
	});
	asr.addEventListener(ASREvents.Result, handleRecognitionResult);
	// Направляем аудио из звонка в ASR
	call.sendMediaTo(asr);
}

// Обрабатываем результат распознавания
function handleRecognitionResult(e) {
	// Останавливаем ASR
	asr.stop(); 
	// Если вероятность более 50%
	if (e.confidence >= 50) {
		city = e.text;
		if (city.toLowerCase() == "петербург") city = "Санкт-Петербург";
		// Проверяем есть ли город в списке городов России
		cityId = getCityId(city);
		if (cityId == -1 && /\s/g.test(city)) {
			Logger.write("Replacing whitespace");
			city = city.replace(/\s/g, "-");
			cityId = getCityId(city);
		}
		if (cityId !== -1) {
			call.say("Вы выбрали город " + city, tts_voice);
			call.addEventListener(CallEvents.PlaybackFinished, handleCityChosen);
		} else {
			call.say("К сожалению, не удалось распознать ваш выбор. Пожалуйста, назовите город, в котором вы хотите узнать погоду еще раз.", tts_voice);
			enableASR();
		}
	} else {
		call.say("К сожалению, не удалось распознать ваш выбор. Пожалуйста, назовите город, в котором вы хотите узнать погоду еще раз.", tts_voice);
		enableASR();
	}
}

// Правильное склонение числительных
function declOfNum(number, titles) {
	cases = [2, 0, 1, 1, 1, 2];
	return titles[(number % 100 > 4 && number % 100 < 20) ? 2 : cases[(number % 10 < 5) ? number % 10 : 5]];
}

// Получаем данные о погоде в выбранном городе
function handleCityChosen(e) {
	call.removeEventListener(CallEvents.PlaybackFinished, handleCityChosen);
	// cityId
	Net.httpRequest("http://export.yandex.ru/weather-ng/forecasts/" + cityId + ".xml", function (e) {
		if (e.code == 200) {
			Logger.write("Weather info has been loaded");

			var data = e.text.split("\n").slice(1).join("\n"),
				forecast = new XML(data),
				ns = new Namespace('w', 'http://weather.yandex.ru/forecast'),
				temperature = forecast.ns::fact.ns::temperature,
				humidity = forecast.ns::fact.ns::humidity,
				pressure = forecast.ns::fact.ns::pressure,
				wind_speed = forecast.ns::fact.ns::wind_speed;

			wind_speed = parseFloat(wind_speed).toFixed(1).replace(/\.0{1}$/, "");
			var forecast_string = "Температура " + parseNumber(temperature) + " " + declOfNum(Math.abs(temperature), ['градус', 'градуса', 'градусов']) + ", " +
				forecast.ns::fact.ns::weather_type + ", влажность " + parseNumber(humidity) + " " + declOfNum(humidity, ['процент', 'процента', 'процентов']) + ", " +
				"атмосферное давление " + parseNumber(pressure) + " " + declOfNum(pressure, ['миллиметр', 'миллиметра', 'миллиметров']) + " ртутного столба, " +
				"скорость ветра " + parseNumber(wind_speed) + " " + declOfNum(wind_speed, ['метр', 'метра', 'метров']) + " в секунду";
			// Озвучиваем и кладем трубку
			call.say(forecast_string, tts_voice);
			call.addEventListener(CallEvents.PlaybackFinished, VoxEngine.terminate);

		} else {
			Logger.write("Couldn't load weather info. Status: " + e.code);
			call.say("К сожалению, сервис погоды временно недоступен. Пожалуйста, повторите попытку позднее.", tts_voice);
			call.addEventListener(CallEvents.PlaybackFinished, VoxEngine.terminate);
		}
	});
}

// Завершение сессии
function cleanup(e) {
	Logger.write("Cleanup");
	VoxEngine.terminate();
}

Не забудьте подставить ваш номер телефона в
call = VoxEngine.callPSTN("ваш номер телефона", "арендованный или верифицированный номер телефона");
, формат номера — 7хххххххххх, ну или другой код страны, если вы не в России. Также укажите номер, с которого будет осуществляться звонок; вы можете подтвердить принадлежащий вам номер через панель управления Voximplant. Сохраняем, можно назвать сценарий SmartIVR. Если вы внимательно смотрели на код, то заметили, что есть функция parseNumber, которая нигде не объявлена. При звонке выполняемые сценарии можно объединять, поэтому мы можем сделать отдельный сценарий parseNumber и вставить туда код этой функции:

var parseNumber = function () {
	var dictionary = [
		["", "один", "два", "три", "четыре", "пять", "шесть", "семь", "восемь", "девять", 
			"десять", "одиннадцать", "двенадцать", "тринадцать", "четырнадцать", "пятнадцать",
			"шестнадцать", "семнадцать", "восемнадцать", "девятнадцать"
		],
		["", "десять", "двадцать", "тридцать", "сорок", "пятьдесят", "шестьдесят", "семьдесят", "восемьдесят", "девяносто"],
		["", "сто", "двести", "триста", "четыреста", "пятьсот", "шестьсот", "семьсот", "восемьсот", "девятьсот"],
		["тысяч|а|и|", "миллион||а|ов", "миллиард||а|ов", "триллион||а|ов"]
	];

	function getNumber(number, limit) {
		var temp = number.match(/^\d{1,3}([,|\s]\d{3})+/);
		if (temp) return temp[0].replace(/[,|\s]/g, "");
		temp = Math.abs(parseInt(number));
		if (temp !== temp || temp > limit) return null;
		return String(temp);
	};

	function setEnding(variants, number) {
		variants = variants.split("|");
		number = number.charAt(number.length - 2) === "1" ? null : number.charAt(number.length - 1);
		switch (number) {
		case "1":
			return variants[0] + variants[1];
		case "2":
		case "3":
		case "4":
			return variants[0] + variants[2];
		default:
			return variants[0] + variants[3];
		};
	};

	function getPostfix(postfix, number) {
		if (typeof postfix === "string" || postfix instanceof String) {
			if (postfix.split("|").length < 3) return " " + postfix;
			return " " + setEnding(postfix, number);
		};
		return "";
	};

	return function (number, postfix) {
		if (typeof number === "undefined")
			return "999" + new Array(dictionary[3].length + 1).join(" 999");
		number = String(number);
		var minus = false;
		number.replace(/^\s+/, "").replace(/^-\s*/, function () {
			minus = true;
			return "";
		});
		number = getNumber(number, Number(new Array(dictionary[3].length + 2).join("999")));
		if (!number) return "";
		postfix = getPostfix(postfix, number);
		if (number === "0") return "ноль" + postfix;
		var position = number.length,
			i = 0,
			j = 0,
			result = [];
		while (position--) {
			result.unshift(dictionary[i++][number.charAt(position)]);
			if (i === 2 && number.charAt(position) === "1")
				result.splice(0, 2, dictionary[0][number.substring(position, position + 2)]);
			if (i === 3 && position !== 0) {
				i = 0;
				if (position > 3 && number.substring(position - 3, position) === "000") {
					j++;
					continue;
				};
				result.unshift(setEnding(dictionary[3][j++], number.substring(0, position)));
			};
		};
		position = result.length - 5;
		switch (result[position]) {
		case "один":
			result[position] = "одна";
			break;
		case "два":
			result[position] = "две";
			break;
		};
		if (minus) result.unshift("минус");
		return result.join(" ").replace(/\s+$/, "").replace(/\s+/g, " ") + postfix;
	};
}();

Сохраняем сценарий, можем назвать его по имени функции parseNumber. Если у кого-то есть желание, то можно модифицировать функцию, чтобы она еще и дробные числа умела переводить в слова :) Осталось создать правила для настройки работы сценариев, после чего можно делать первый тестовый звонок себе на телефон. Для этого идем в раздел «Роутинг» и создаем новое правило. Назвать его можно как угодно, так как мы делаем сейчас правило для исходящего звонка, то можем назвать его OutboundTest, а маску оставить .*, так как при запуске сценариев через HTTP API маска не учитывается (в запросе указывается id правила). Прикрепляем к нему parseNumber и SmartIVR и сохраняем (см. скриншот)


Если после этого навести курсор на вновь созданное правило, то мы увидим 3 кнопки — для запуска, редактирования и удаления. Нас интересует первая.


Появляется диалог, который позволяет, по сути, удобно вызывать метод StartScenarios, описанный в HTTP API.


Если вы правильно указали номер телефона, то после нажатия на кнопку Run вам придет входящий звонок и попросят назвать город России, если система распознает город и найдет его в БД Яндекс Погоды, то потом расскажут про текущую погоду в этом городе. Конечно, можно взять номер телефона и прицепить его к данному сценарию, чтобы IVR работал для входящих звонков, в этом случае не забудьте закомментировать обработчик AppEvents.Started, а то при каждом звонке на IVR система будет делать еще и исходящий звонок на ваш номер :) Номер привязывается к приложению в разделе «Номера» в приложении. После этого нужно сделать правило приложения, которое входящие звонки на номер будет отправлять к нашим сценариям, в маску можно просто написать номер телефона.

P.S. Почитать про создание обычных IVRов с меню с помощью VoxImplant можно в нашем блоге
Voximplant
109,89
Облачная платформа голосовой и видеотелефонии
Поделиться публикацией

Комментарии 2

    0
    Вы хотя бы поддерживали в актуальном состоянии свои примеры. А то у вас или не работают или номера доступа не доступны.
      0
      Да, номер был двухлетней давности и уже «утилизировался» на благие цели. Номер убрали, а вот пример должен замечательно работать, потому что обратная совместимость сценариев! Если какие вопросы — пишите в личку или на support@voximplant.com — расскажем и покажем!

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое