Alexa 自作スキルで音楽再生
flask-askを使って再生させてみます。pythonのサーバー機能を使い、同一ポートで実装しています。スキル応答(音楽の停止等)タイミングが悪いと受け付けないバグがありますが、バ垂れ流しだと問題ないので。
仕組み
flask-askのサンプルプログラムを元に、playlistのURLを./music内にあるファイルをリストとして取得し、これをURL化します。その後、AlexaのMP3の要求に対して、そのファイルを提供します。
pythonのファイルサーバープログラムはこちらを、
./musicのファイル一覧取得にはこちらを参考にしました。
プログラム
pythonサーバー
# -*- coding: utf-8 -*- import collections import logging import os from copy import copy from flask import Flask, json,send_file, make_response, send_from_directory from flask_ask import Ask, question, statement, audio, current_stream, logger import glob app = Flask(__name__) app.config['JSON_AS_ASCII'] = False ask = Ask(app, "/") logging.getLogger('flask_ask').setLevel(logging.INFO) def get_music_file_list(music_file): return glob.glob(music_file+"/*") def get_music_file_url_list(music_file_path): result=[] for file_path in music_file_path: result.append("https://"ドメイン名"/music/"+file_path.split('/')[-1]) return result playlist=(get_music_file_url_list(get_music_file_list("./music"))) """ playlist = [ # 'https://www.freesound.org/data/previews/367/367142_2188-lq.mp3', 'https://archive.org/download/mailboxbadgerdrumsamplesvolume2/Ringing.mp3', 'https://archive.org/download/petescott20160927/20160927%20RC300-53-127.0bpm.mp3', 'https://archive.org/download/plpl011/plpl011_05-johnny_ripper-rain.mp3', 'https://archive.org/download/piano_by_maxmsp/beats107.mp3', 'https://archive.org/download/petescott20160927/20160927%20RC300-58-115.1bpm.mp3', 'https://archive.org/download/PianoScale/PianoScale.mp3', # 'https://archive.org/download/FemaleVoiceSample/Female_VoiceTalent_demo.mp4', 'https://archive.org/download/mailboxbadgerdrumsamplesvolume2/Risset%20Drum%201.mp3', 'https://archive.org/download/mailboxbadgerdrumsamplesvolume2/Submarine.mp3', # 'https://ia800203.us.archive.org/27/items/CarelessWhisper_435/CarelessWhisper.ogg' ] """ class QueueManager(object): """Manages queue data in a seperate context from current_stream. The flask-ask Local current_stream refers only to the current data from Alexa requests and Skill Responses. Alexa Skills Kit does not provide enqueued or stream-histroy data and does not provide a session attribute when delivering AudioPlayer Requests. This class is used to maintain accurate control of multiple streams, so that the user may send Intents to move throughout a queue. """ def __init__(self, urls): self._urls = urls self._queued = collections.deque(urls) self._history = collections.deque() self._current = None @property def status(self): status = { 'Current Position': self.current_position, 'Current URL': self.current, 'Next URL': self.up_next, 'Previous': self.previous, 'History': list(self.history) } return status @property def up_next(self): """Returns the url at the front of the queue""" qcopy = copy(self._queued) try: return qcopy.popleft() except IndexError: return None @property def current(self): return self._current @current.setter def current(self, url): self._save_to_history() self._current = url @property def history(self): return self._history @property def previous(self): history = copy(self.history) try: return history.pop() except IndexError: return None def add(self, url): self._urls.append(url) self._queued.append(url) def extend(self, urls): self._urls.extend(urls) self._queued.extend(urls) def _save_to_history(self): if self._current: self._history.append(self._current) def end_current(self): self._save_to_history() self._current = None def step(self): self.end_current() self._current = self._queued.popleft() return self._current def step_back(self): self._queued.appendleft(self._current) self._current = self._history.pop() return self._current def reset(self): self._queued = collections.deque(self._urls) self._history = [] def start(self): self.__init__(self._urls) return self.step() @property def current_position(self): return len(self._history) + 1 queue = QueueManager(playlist) @app.route('/music/<string:report_id>', methods=['GET']) def report3(report_id): response = make_response() response.data = open("./music/"+report_id, "rb").read() print("./music/"+report_id) downloadFileName = report_id response.headers['Content-Disposition'] = 'attachment; filename=' return response @ask.launch def launch(): card_title = 'Playlist Example' text = 'Welcome to an example for playing a playlist. You can ask me to start the playlist.' prompt = 'You can ask start playlist.' return question(text).reprompt(prompt).simple_card(card_title, text) @ask.intent('PlaylistDemoIntent') def start_playlist(): speech = 'Heres a playlist of some sounds. You can ask me Next, Previous, or Start Over' stream_url = queue.start() print(stream_url) return audio(speech).play(stream_url) # QueueManager object is not stepped forward here. # This allows for Next Intents and on_playback_finished requests to trigger the step @ask.on_playback_nearly_finished() def nearly_finished(): if queue.up_next: _infodump('Alexa is now ready for a Next or Previous Intent') # dump_stream_info() next_stream = queue.up_next _infodump('Enqueueing {}'.format(next_stream)) return audio().enqueue(next_stream) else: _infodump('Nearly finished with last song in playlist') @ask.on_playback_finished() def play_back_finished(): _infodump('Finished Audio stream for track {}'.format(queue.current_position)) if queue.up_next: queue.step() _infodump('stepped queue forward') dump_stream_info() else: return statement('You have reached the end of the playlist!') # NextIntent steps queue forward and clears enqueued streams that were already sent to Alexa # next_stream will match queue.up_next and enqueue Alexa with the correct subsequent stream. @ask.intent('AMAZON.NextIntent') def next_song(): if queue.up_next: speech = 'playing next queued song' next_stream = queue.step() _infodump('Stepped queue forward to {}'.format(next_stream)) dump_stream_info() return audio(speech).play(next_stream) else: return audio('There are no more songs in the queue') @ask.intent('AMAZON.PreviousIntent') def previous_song(): if queue.previous: speech = 'playing previously played song' prev_stream = queue.step_back() dump_stream_info() return audio(speech).play(prev_stream) else: return audio('There are no songs in your playlist history.') @ask.intent('AMAZON.StartOverIntent') def restart_track(): if queue.current: speech = 'Restarting current track' dump_stream_info() return audio(speech).play(queue.current, offset=0) else: return statement('There is no current song') @ask.on_playback_started() def started(offset, token, url): _infodump('Started audio stream for track {}'.format(queue.current_position)) dump_stream_info() @ask.on_playback_stopped() def stopped(offset, token): _infodump('Stopped audio stream for track {}'.format(queue.current_position)) @ask.intent('AMAZON.PauseIntent') def pause(): seconds = current_stream.offsetInMilliseconds / 1000 msg = 'Paused the Playlist on track {}, offset at {} seconds'.format( queue.current_position, seconds) _infodump(msg) dump_stream_info() return audio(msg).stop().simple_card(msg) @ask.intent('AMAZON.ResumeIntent') def resume(): seconds = current_stream.offsetInMilliseconds / 1000 msg = 'Resuming the Playlist on track {}, offset at {} seconds'.format(queue.current_position, seconds) _infodump(msg) dump_stream_info() return audio(msg).resume().simple_card(msg) @ask.session_ended def session_ended(): return "{}", 200 def dump_stream_info(): status = { 'Current Stream Status': current_stream.__dict__, 'Queue status': queue.status } _infodump(status) def _infodump(obj, indent=2): msg = json.dumps(obj, indent=indent) logger.info(msg) if __name__ == '__main__': if 'ASK_VERIFY_REQUESTS' in os.environ: verify = str(os.environ.get('ASK_VERIFY_REQUESTS', '')).lower() if verify == 'false': app.config['ASK_VERIFY_REQUESTS'] = False app.run(debug=True,host="0.0.0.0")
アマゾンのスキルJSON
{ "interactionModel": { "languageModel": { "invocationName": "テスト", "intents": [ { "name": "AMAZON.HelpIntent", "samples": [] }, { "name": "PlaylistDemoIntent", "slots": [ { "name": "music", "type": "AMAZON.SearchQuery" } ], "samples": [ "再生" ] }, { "name": "AMAZON.StopIntent", "samples": [] }, { "name": "AMAZON.CancelIntent", "samples": [] }, { "name": "AMAZON.PauseIntent", "samples": [] }, { "name": "AMAZON.ResumeIntent", "samples": [] }, { "name": "AMAZON.PreviousIntent", "samples": [] }, { "name": "AMAZON.StartOverIntent", "samples": [] }, { "name": "AMAZON.NextIntent", "samples": [] } ], "types": [] } } }
まとめ
動作が結構怪しいですが、mp3の320kbpsがと最後まで再生できます。持ってる曲聞くのに有料サービス面倒だし・・・スキルからの応答に時間がかかりますエラーが出るのは、mp3でコネクションいるからかな?
(追記)
スキル応答部分とファイル提供部分を分離して実行したらうまくいきました。flaskよ、マルチスレッドになれ
ディスカッション
コメント一覧
まだ、コメントがありません