.NET Fest: полная программа конференции на сайте. Присоединяйся к самому большому .NET ивенту
×Закрыть

Як змусити Amazon Alexa грати музику з Google Music, хоч вона цього й не хоче

Привіт, мене звати Олег Шанковський, я Java-програміст. Працюю в Києві в американській компанії, що спеціалізується на кібербезпеці. У цьому матеріалі розповім, як змусити розумну голосову асистентку Алексу від Amazon програвати музику з Google Music, як обійти пов’язані проблеми, а також навіщо це все потрібно.

Із чим маємо справу

Серед популярних голосових асистентів (Siri від Apple, Assistant від Google, чи, Боже збав, Cortana від Microsoft) Алексу вважають найрозумнішою й найкориснішою. Одна з основних причин — можливість навчати її новим умінням і відносна легкість цього процесу. Ви можете додати до вашої колонки нове вміння з великого магазину скілів, а якщо не знайдете там потрібний — створити скіл самотужки. Саме цим ми сьогодні й займемося.

Понад рік я вдома користуюся двома колонками Amazon Echo Dot, які під’єднано до стереосистем на кухні й у вітальні. Мої колонки вміють умикати/вимикати телевізор та андроїд-приставку до нього (Alexa, TV on), знаходити мій телефон (Alexa, ask Tracker to ring my phone), програвати подкасти й аудіокниги, розповідати про погоду, читати «Вікіпедію» тощо. Колонки знають одна про одну, уміють одна одну вмикати/вимикати (Alexa, stop in the kitchen), а також грати музику синхронно (Alexa, play Nirvana everywhere).

Основною причиною покупки колонок, звичайно, було бажання слухати музику. Однак саме із цим і виникло найбільше проблем. По-перше, Алекса офіційно в Україні не підтримується. Через це під час реєстрації вам будуть недоступні більшість музичних сервісів, а з тих, що лишаться (здається, лише радіо TuneIn) користі не буде практично жодної.

Цю проблему я розв’язав, указавши в реєстраційних даних регіон США й адресу супермаркету в Чикаго. Завдяки цьому мені, як новоспеченому американцеві, став доступним цілий набір музичних сервісів, серед яких найкорисніші — Pandora й iHeartRadio. На цих сервісах є чимало справді хорошої музики, і цим можна було б й обмежитися, якби не одна проблема. Річ у тім, що ви можете задати Алексі лише виконавця, а не конкретну пісню. З указаного вами виконавця Алекса створить треклист, у який, окрім бажаного артиста, додасть пісні такого ж жанру інших виконавців. Наприклад, у відповідь на команду Alexa, play Metallica on iHeart, окрім самої Metallica, ви почуєте Black Sabbath, Nirvana, Scorpions і навіть Queen.

Для більшості випадків такої поведінки повністю достатньо: замовили бажаний жанр і займаєтеся своїми справами під хорошу музику. Однак, якщо вам раптом захочеться послухати саме Unforgiven II, Алекса у відповідь запропонує купити платну передплату на Amazon Music за 10 доларів на місяць. Не те щоб це було дорого, однак я вже оплачую преміум-передплату Google і купувати ще одну, лише щоб слухати її вдома, мені не хотілося. Найочевиднішим рішенням було б зактивувати на Алексі скіл Google Music, однак такого просто не існує. Кажуть, причина в конкуренції компаній Google та Amazon.

Ну що ж, challenge accepted! Створимо скіл самотужки.

Створюємо скіл

Для того щоб у відповідь на наш голосовий запит Алекса почала програвати задану пісню, ми повинні дати посилання на неї. Тобто нам потрібен програмний інтерфейс, який знайде й поверне нам це посилання. Тут ми стикаємося зі ще однією проблемою: офіційного Google Music API не існує. Є офіційний API для YouTube, однак він повертає посилання на відео-, а не на аудіофайл. Після певного дослідження я дійшов висновку, що компанія Google доклала чимало зусиль, щоб ускладнити виділення аудіопотоку з відеофайлу (як мінімум, це було б незручно з погляду часових затрат на виконання команди), й облишив цю справу.

Після коротких додаткових пошуків виявив, що є хороший неофіційний API для Google Music, який написано на Python, а також Java wrapper до нього. Останнє — саме те, що нам і потрібно, беремо.

Починаємо створення скіла. Для цього заходимо на Alexa Developer Console і реєструємо там акаунт, до якого прив’язано нашу розумну колонку. Після реєстрації потрапляємо в консоль, у якій бачимо кнопку Create Skill.

Натискаємо її, уводимо потрібну назву скіла, залишаємо тип моделі Custom, а мову взаємодії — англійську. У наступному вікні вибираємо Start from scratch. Потрапляємо в головну адмінку нашого скіла.

Починаємо із секції Invocation, де вказуємо, яке звернення активуватиме наш скіл (Alexa, ask Google Music to play...), і вибираємо Google Music.

Переходимо до секції Intents, де вказуємо основні параметри взаємодії з нашим скілом. Intent — це намір, і тут Алекса дізнається, що ви хочете зробити. Є низка убудованих інтентів, які ми можемо використовувати під час створення скіла, наприклад, AMAZON.StopIntent зупинить виконання певної дії, AMAZON.PauseIntent поставить її на паузу тощо. Назва інтента — це не голосова команда, тому називаємо його так, як нам до вподоби. Наприклад GoogleMusic. Натискаємо Create custom intent.

Потім консоль пропонує нам вказати Sample Utterances, тобто голосову фразу, що зактивує інтент і вкаже йому, що слід зробити. Нас цікавить лише одна команда — play. Пишемо її.

Далі нам потрібна змінна, щоб передати назву пісні, яку ми хочемо послухати. Такі змінні називають слотами. Можна було б указати два слоти: окремо для пісні й виконавця, однак це зробило б взаємодію з Алексою менш зручною. Тим паче, що наш Google Music API чудово вміє шукати практично за будь-якою побудовою фрази. Тому залишаємо один слот і називаємо його song. Окрім назви, нам також потрібно вибрати один з наявних типів слота. Загалом тип (наприклад, Actor, Airline, Book, Drink, Duration) може потім допомогти Алексі краще визначити, що саме ми хочемо, і виконати команду якісніше, але, оскільки наш пошук здійснюватиме не Алекса, а Google, то для нас це не має жодного значення. Як тип я вибрав AMAZON.MusicCreativeWorkType.

Повертаємося нагору до нашого Sample Utterances, який ми назвали play, й у фігурних дужках після нього вписуємо наш слот song.

Це означає, що фразу, яку ми промовимо після слова play, буде записано в слот song. Тиснемо кнопку Save Model угорі.

Переходимо в розділ Interfaces, активуємо Audio Player і тиснемо Save Interfaces.

Переходимо в розділ Endpoint. Тут ми повинні вказати, де живе бекенд, який обробить нашу команду й поверне відповідь. У нас є два варіанти: AWS Lambda ARN i HTTPS. Оскільки другий варіант означає розгортання сервера, ми зупиняємося на першому, дуже простому й зручному для наших цілей. Однак Lambda слід спершу налаштувати, тому поки відкладаємо консоль Алекси й в сусідній вкладці відкриваємо консоль Amazon Web Services (AWS).

Якщо у вас ще немає акаунта в AWS, саме час його створити. Якщо ж є, можливо, є сенс створити новий. У всякому разі, я для більшости своїх AWS-проектів створюю нові акаунти на окремі поштові адреси, щоб не робити проекти залежними один від одного. Наприклад, іноді завдяки такому підходу можна непогано заощадити.

Під час реєстрації Amazon попросить вас указати дані кредитної картки на випадок, якщо захочете користуватися платними сервісами. Ці дані вказати доведеться, однак гроші на цьому проекті ви точно не потратите: Lambda надає безоплатно 1 млн запитів або 400 тис. Гб/с на місяць. Розслабляємося й реєструємося.

Зайшовши в консоль AWS, знаходимо серед сервісів Lambda, переходимо в розділ Functions і тиснемо кнопку Create function. Серед варіантів створення функції залишаємо активним Author from scratch, указуємо назву функції (наприклад googleMusic) і вибираємо мову Java 8. У розділі Choose or create an execution role вибираємо Create a new role with basic lambda permissions. Тиснемо Create function.
Потрапляємо в консоль нашої нової функції. Найперше нам треба вказати, що саме її активуватиме. Тиснемо Add trigger і вибираємо Alexa Skills Kit. Повертаємося в консоль Алекси, знаходимо в розділі Endpoint рядок з ID нашого скіла (amzn1.ask.skill...), копіюємо, повертаємося в консоль Lambda й уставляємо його в поле Skill ID.

Тиснемо Save угорі екрана.

Поруч із кнопкою Save бачимо ідентифікатор нашої функції arn:aws:lambda:... Копіюємо цей рядок і повертаємося в консоль Алекси. Тут, у розділі Endpoint, уставляємо рядок у поле Default Region.

Тепер наші скіл і функція Lambda зв’язані між собою. Переходимо в розділ Intents і тиснемо кнопку Build model.

Вітаю, ваш скіл створено, і він уже навіть доступний на вашій розумній колонці. Правда, поки що він нічого не вміє. Навчімо.

Пишемо бекенд

Я писатиму бекенд на Java в середовищі IntelliJ IDEA, а проект збиратиму за допомогою Maven.

Створюємо звичайний Java-проект. Найперше додаємо всі потрібні залежності. Для роботи з Алексою й Lambda нам потрібно:

<dependency>
  <groupId>com.amazon.alexa</groupId>
  <artifactId>alexa-skills-kit</artifactId>
  <version>1.8.1</version>
</dependency>

<dependency>
  <groupId>com.amazonaws</groupId>
  <artifactId>aws-lambda-java-core</artifactId>
  <version>1.2.0</version>
</dependency>

<dependency>
  <groupId>com.amazonaws</groupId>
  <artifactId>aws-lambda-java-events</artifactId>
  <version>2.2.6</version>
</dependency>

Під’єднуємо неофіційний Google Music API:

<dependency>
  <groupId>com.github.felixgail</groupId>
  <artifactId>gplaymusic</artifactId>
  <version>0.3.6</version>
</dependency>

Згодом під час деплою готового коду на Lambda я стикнувся з проблемою: функція не могла знайти шлях до основного хендлера. Поґуґливши, я виявив, що ця проблема є типовою і її розв’язують пакуванням проекту в over-jar з усіма залежностями замість звичайного jar. Для цього додаємо плагін shade для Maven:

<build>
  <plugins>
     <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>3.2.1</version>
        <executions>
           <execution>
              <phase>package</phase>
              <goals>
                 <goal>shade</goal>
              </goals>
           </execution>
        </executions>
     </plugin>
  </plugins>
</build>

Переходимо до написання коду. Нам потрібно створити спічлет (ну, ви зрозуміли: аплет, сервлет, спічлет...).

Наш клас повинен зімплементити інтерфейс SpeechletV2 й заоверрайдити чотири методи:

  • onSessionStarted;
  • onLaunch;
  • onIntent;
  • onSessionEnded.

onSessionStarted

У методі onSessionStarted відбуваються ініціалізація скіла і його підготовка до роботи. Тут ми повинні налаштувати основний об’єкт GPlayMusic, який і шукатиме музику за нашим запитом. Для цього нам слід залогінитися в Google.

Передусім ми повинні створити токен (об’єкт AuthToken), надавши йому адресу Google-пошти, пароль від неї (panic mode: on!) й IMEI мобільного пристрою, на якому зараз чи колись було встановлено додаток Google Play Music.

Забігаючи наперед, скажу, що ви зможете успішно протестувати такий логін з локального комп’ютера, однак після деплою вашого коду на Lambda нічого не працюватиме. Річ у тім, що Google не зрозуміє, як це ви, щойно бувши в Україні, раптом логінитеся з Північної Вірджинії. Навіть після підтвердження з вашого боку, що це справді були ви й усе гаразд, Google не дозволить такий логін.

Розв’язати цю проблему можна, створивши спеціальний пароль для додатків. Для цього вам слід зайти в Google-акаунт, перейти в розділ «Безпека» й вибрати «Пароль додатків». У відповідь Google створить спеціальний 16-значний пароль, який ви зможете використати під час створення AuthToken та успішно залогінитися у вашому Google-акаунті. Отже, ви не передаєте в програму ваш справжній пароль.

Однак і тут є нюанс: створити пароль додатків можливо лише для акаунта, у якого ввімкнено двофакторну авторизацію. Тому вам доведеться її ввімкнути. Тож парадоксально, але наш скіл навіть підвищить безпеку вашого Google-акаунта, змусивши вас перейти на безпечніший варіант авторизації.

Звичайно, ми не прописуватимемо облікові дані просто в коді. На Lambda можна вказати змінні середовища (Environment variables), у які ми й упишемо наші дані: USER_NAME, USER_PASSWORD та IMEI). Також Lambda дає змогу зенкриптити ці дані.

Отже, логінимося:

AuthToken token = null;
try {
   token = TokenProvider.provideToken(System.getenv("USER_NAME"), System.getenv("USER_PASSWORD"), System.getenv("IMEI"));
} catch (IOException | Gpsoauth.TokenRequestFailed e) {
   log.error("Error while auth token generating", e);
}

api = new GPlayMusic.Builder()
   .setAuthToken(token)
   .build();

На цьому завдання методу onSessionStarted виконано, переходимо до onLaunch.

onLaunch

Метод onLaunch викликають за допомогою команди Alexa, open Google Music. У відповідь ми просто привітаємося.

Алекса надсилає нам запит від користувача в об’єкті SpeechletRequestEnvelope. У відповідь ми повинні надіслати об’єкт SpeechletResponse. У цей об’єкт ми повинні помістити все, що потрібно нашій колонці для відповіді користувачу. У цьому разі це буде просто вітання, яке ми помістимо в об’єкт PlainTextOutputSpeech. Окрім самого вітання, ми додамо repromptSpeech — додаткову фразу, якою Алекса пояснить користувачу, що саме він може робити:

PlainTextOutputSpeech speech = new PlainTextOutputSpeech();
speech.setText(WELCOME_TEXT);
PlainTextOutputSpeech repromptSpeech = new PlainTextOutputSpeech();
repromptSpeech.setText(CHOOSE_THE_SONG_REQUEST);
Reprompt reprompt = new Reprompt();
reprompt.setOutputSpeech(repromptSpeech);

return SpeechletResponse.newAskResponse(speech, reprompt);

Варто зазначити, що метод onLaunch не спрацює, якщо користувач відразу попросить Алексу ввімкнути конкретну пісню на нашому скілі (Alexa, ask Google Music to play...). У такому разі ми відразу перейдемо до методу onIntent.

onIntent

Основна робота відбувається саме тут. Разом із запитом ми отримаємо об’єкт Intent, який містить у собі назву інтента й слот. Ми очікуємо від користувача дві дії: програвати музику й зупинити її. Якщо користувач хоче програвати музику, витягуємо з інтента слот і передаємо його як пошуковий запит у наш Google Music API. Якщо користувач захотів тиші, вішаємо на наш SpeechletResponse текст Goodbye і надсилаємо Алексі директиву StopDirective. Якщо нам почулося щось інше, повідомляємо користувача про помилку й просимо повторити.

@Override
public SpeechletResponse onIntent(SpeechletRequestEnvelope<IntentRequest> requestEnvelope) {
   logMethodStart("onIntent", requestEnvelope);
   IntentRequest request = requestEnvelope.getRequest();
   Intent intent = request.getIntent();
   String name = intent.getName();
   log.info("Requested intent: {}", name);

   switch (name) {
       case GOOGLE_MUSIC_INTENT:
           String song = intent.getSlot(SONG_SLOT).getValue();
           try {
               return playMusicResponse(song);
           } catch (Exception e) {
               log.error("Couldn't play {}", song, e);
               return newAskRequest(ERROR);
           }
       case "AMAZON.StopIntent":
       case "AMAZON.CancelIntent":
           return goodbye();
       default:
           log.error("Unexpected intent: " + name);
           return newAskRequest(WRONG_REQUEST);
   }
}

Розглянемо метод playMusicResponse, який і здійснює пошук. Найперше ми передаємо розпізнаний Алексою запит користувача на пісню, яку він хоче слухати, як вхідний параметр нашому API для пошуку. У відповідь API може повернути великий список треків, але, оскільки ми хочемо слухати саме ту пісню, яку замовили, то зважаємо на те, що запит було задано максимально коректно й перший результат у видачі є найрелевантнішим, тому обмежуємо кількість результатів одним. Якщо список виявився порожнім, повідомляємо користувача, що нічого знайти не вдалося.

Якщо результати все ж є, нам потрібно створити декілька різних об’єктів і передати їх Алексі для програвання музики:

  • Stream, що міститиме посилання на пісню й кілька додаткових технічних параметрів;
  • AudioItem, що міститиме Stream;
  • PlayDirective — команда Алексі програвати музику, що міститиме AudioItem, а також деякі додаткові параметри;
  • SpeechletResponse, що міститиме об’єкт PlayDirective, а також текст із назвою пісні й ім’ям виконавця, який Алекса промовить перед початком програвання музики.

Ось який це має вигляд:

private SpeechletResponse playMusicResponse(String songRequest) throws Exception {
   List<Track> trackList = api.getTrackApi().search(songRequest, 1);
   if (trackList.isEmpty())
       return noTrackFoundResponse(songRequest);

   Track track = trackList.get(0);

   Stream stream = new Stream();
   stream.setUrl(track.getStreamURL(StreamQuality.HIGH).toString());
   stream.setOffsetInMilliseconds(0);
   stream.setExpectedPreviousToken(null);
   stream.setToken("0");

   AudioItem song = new AudioItem();
   song.setStream(stream);

   PlayDirective directive = new PlayDirective();
   directive.setAudioItem(song);
   directive.setPlayBehavior(PlayBehavior.REPLACE_ALL);

   SpeechletResponse response = new SpeechletResponse();
   response.setDirectives(singletonList(directive));
   response.setNullableShouldEndSession(null);

   PlainTextOutputSpeech speech = new PlainTextOutputSpeech();
   speech.setText("Playing " + track.getTitle() + " by " + track.getArtist());

   response.setOutputSpeech(speech);

   return response;
}

onSessionEnded

У нашому випадку додаткові дії в цьому методі не потрібні, тому просто логуємо його виклик.

Крім класу GoogleMusicSpeechlet, нам слід створити ще один, який наслідуватиме клас SpeechletRequestStreamHandler і стане точкою входу для Алекси. Єдине його завдання — зберігати ID скіла, який може викликати цей функціонал.

public class GoogleMusicRequestStreamHandler extends SpeechletRequestStreamHandler {
   private static final Set<String> supportedApplicationIds = new HashSet<>();

   static {
       supportedApplicationIds.add("amzn1.ask.skill.da7a7858-5bf8-46be-a12a-30f85a7b3283");
   }

   public GoogleMusicRequestStreamHandler() {
       super(new GoogleMusicSpeechlet(), supportedApplicationIds);
   }
}

Наш бекенд готовий. Приправити логами за смаком, додати трохи юніт-тестів і можна подавати. Білдимо jar (mvn package) і деплоїмо його на Lambda.

У полі Runtime вибираємо Java 8, у полі Handler указуємо повністю шлях до GoogleMusicRequestStreamHandler і завантажуємо наш jar.

Готово. Тепер наш скіл доступний на колонці й готовий виконати замовлення. Крім того, тестувати його можна через вебінтерфейс у консолі Алекси:

Іноді під час тестування скіла ви можете отримати помилку, а в логах з’явиться повідомлення про збій автентифікації. Це відбувається тому, що змінні оточення ще не підвантажилися. Зазвичай перезавантаження браузера розв’язує проблему.

Через обмеження браузера власне програвання музики не розпочнеться, однак такого тестування здебільшого достатньо, щоб не бігати щоразу до колонки. Також тестувати скіл можна через мобільний додаток Reverb for Amazon Alexa, який чудово імітує спілкування з Алексою.

Логи можна читати на CloudWatch Logs.

Код бекенду.

Що далі?

Звичайно, ми не можемо опублікувати скіл у магазині Amazon: компанія не затвердить його зі зрозумілих причин. Однак спокійно можемо користуватися ним удома в режимі In Development.

Чи можна щось удосконалити в нашому скілі? Звісно. На момент написання цієї статті скіл не вміє працювати з командами next, previous, pause і repeat та не вміє створювати плейлисти. Усе це можна досить легко зреалізувати й, можливо, колись це зроблю, однак наразі скіл виконує основну функцію, заради якої його й створював: дає змогу слухати саме ту музику, яку я хочу тут і зараз.

Alexa, play We are the champions.

LinkedIn

8 комментариев

Подписаться на комментарииОтписаться от комментариев Комментарии могут оставлять только пользователи с подтвержденными аккаунтами.
Загалом тип (наприклад, Actor, Airline, Book, Drink, Duration) може потім допомогти Алексі краще визначити, що саме ми хочемо, і виконати команду якісніше, але, оскільки наш пошук здійснюватиме не Алекса, а Google, то для нас це не має жодного значення.

Тут я не можу погодитись. Від того, який тип слоту буде обрано, залежить кінцева строка, що відправиться на бекенд, та чи взагалі вона буде розпізнана. Раніше був чудовий тип AMAZON.LITERAL саме для цієї задачі, але минулого року його зарізали :(

Дякую, хороше уточнення)

А у вас есть такой-же но с перламутровыми пуговицами? В смысле — спотифай.

Спотіфай доступний з коробки, але для цього потрібна його преміум-підписка.

Ясно, тогда будем искать.

Технічна, фанова стаття на доу, та щей українською! Дякую :)

Подписаться на комментарии