Як змусити 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.

Переходимо в розділ Security. Тут ми повинні вказати, де живе бекенд, який обробить нашу команду й поверне відповідь. У нас є два варіанти: 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

У методі 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 намір:" + name);
 return newAskRequest(WRONG_REQUEST);
}
}

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

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

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

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.

Опубліковано: 30/08/19 @ 07:00
Розділ Пошуковики

Рекомендуємо:

5 кращих книг для вивчення JavaScript від Senior Front-end розробника Олександра Головатого
Роль Product Manager на різних етапах розвитку проекту
Переїзд в Люблін: про роботу в ІТ, спорт і розваги
Java дайджест #44: Java 13, Micronaut Predator і смерть Mercurial
Розробка API на Python із Serverless