Reproduzindo episódios de um podcast em um skill Alexa

Nesta série, estamos mostrando como implementamos uma skill para Alexa que reproduz os Drops da EximiaCo. Ela está disponível no Marketplace da Alexa.

No post anterior, indicamos que a primeira interação com uma skill da Alexa deve retornar uma mensagem de boas-vindas e também instruções de utilização, proporcionando uma experiência agradável para os usuários.

A sequência, em nosso projeto, são dois intents principais para seleção e reprodução de episódios. O primeiro, PlayLatestEpisode, faz com que a Alexa reproduza o episódio mais recente. O segundo, SearchEpisode, permite pesquisar um episódio baseado em um tema específico. Além disso, é necessário ativar a interface de AudioPlayer da Alexa que fornece intents nativos para avançar, retroceder, parar e continuar.

Para a execução da mídia, construímos uma abstração chamada Player, que permite tratar eventos relacionados e comandar a reprodução.


class Player(object):
    def __init__(self, handler_input):
        self.episodes_provider = EpisodesProvider()
        self.handler_input = handler_input
        self.state = PlayerState(handler_input.attributes_manager, self.episodes_provider)

    def reset(self):
        self.state.set_offset(0)

    # user interaction
    def play(self, episode):
        if episode is None:
            return False

        self.handler_input.response_builder.add_directive(
            PlayDirective(
                play_behavior=PlayBehavior.REPLACE_ALL,
                audio_item=AudioItem(
                    stream=Stream(
                        token=episode["pub"],
                        url=episode["address"],
                        offset_in_milliseconds=self.state.get_offset(),
                        expected_previous_token=None),
                    metadata=None))
        ).set_should_end_session(True)

        return True

    def play_latest(self):        
        episode = self.episodes_provider.get_latest()
        return self.play(episode)

    def is_playing_episode(self):
        current_episode = self.state.get_current_episode()
        if current_episode is None:
            return False
        return True

    def stop(self):
        self.handler_input.response_builder.add_directive(StopDirective())

    def resume(self):
        self.play(self.state.get_current_episode())

    def previous(self, jump_current_episode=True):
        current_episode = self.state.get_current_episode()
        if current_episode is None:
            return False

        self.reset()

        if jump_current_episode:
            self.disable_repeat()

            episode = self.episodes_provider.get_previous(current_episode)
            if episode is None and self.state.get_loop():
                episode = self.episodes_provider.get_latest()

            return self.play(episode)

        return self.play(current_episode)

    def next(self):
        current_episode = self.state.get_current_episode()
        if current_episode is None:
            return False

        self.reset()
        self.disable_repeat()

        episode = self.episodes_provider.get_next(current_episode)
        if episode is None and self.state.get_loop():
            episode = self.episodes_provider.get_first()

        return self.play(episode)

    # handlers
    def handle_playback_started(self):
        current_token = self.handler_input.request_envelope.request.token
        self.state.set_token(current_token)
        self.state.set_current_episode(current_token)

    def handle_playback_nearly_finished(self):
        current_episode = self.state.get_current_episode()
        if current_episode is None:
            return

        previous_episode = current_episode

        if not self.state.get_repeat():
            previous_episode = self.episodes_provider.get_previous(current_episode)

        if previous_episode is None and self.state.get_loop():
            previous_episode = self.episodes_provider.get_latest()

        if previous_episode is None:
            return

        self.handler_input.response_builder.add_directive(
            PlayDirective(
                play_behavior=PlayBehavior.ENQUEUE,
                audio_item=AudioItem(
                    stream=Stream(
                        token=previous_episode["pub"],
                        url=previous_episode["address"],
                        offset_in_milliseconds=0,
                        expected_previous_token=current_episode["pub"]),
                    metadata=None))
        ).set_should_end_session(True)

    def handle_playback_finished(self):
        self.reset()

    def handle_playback_stopped(self):
        millis = self.handler_input.request_envelope.request.offset_in_milliseconds
        print("handle_playback_stopped: {} millis".format(millis))
        self.state.set_offset(millis)

    # playback state
    def enable_repeat(self):
        self.state.set_repeat(True)

    def disable_repeat(self):
        self.state.set_repeat(False)

    def enable_loop(self):
        self.state.set_loop(True)

    def disable_loop(self):
        self.state.set_loop(False)

Explicando: no intent PlayLatestEpisode, será obtido o episódio mais novo da base de dados para ser passado como parâmetro no PlayDirective, instruindo a Alexa a iniciar a reprodução do aúdio (MP3).

Um ponto muito importante na execução, é o Token enviado a esta diretiva. Ele garante a ordem em que os episódios são executados, para que a Alexa não confunda os episódios em determinadas situações, principalmente quando mandamos avançar e retroceder.

class StartLatestEpisodeHandler(AbstractRequestHandler):
    def can_handle(self, handler_input):
        return ask_utils.is_intent_name("PlayLatestEpisode")(handler_input)

    def handle(self, handler_input):
        player = Player(handler_input)        
        player.play_latest()

        return handler_input.response_builder.response

Já a implementação do intent SearchEpisode é um pouco mais complexa. Primeiramente, é utilizado o conteúdo slot episode para buscar o episódio na base de dados. Se forem retornados diversos resultados, os mesmos são armazenados na sessão da skill, e como resposta é solicitado ao usuário um refinamento da pesquisa. Quando é recebida a resposta do usuário no segundo estágio da pesquisa, utilizamos a biblioteca Whoosh do Python para selecionar o episódio que mais se aproxima da solicitação do usuário, e iniciamos a reprodução.

class SearchEpisodeHandler(AbstractRequestHandler):
def can_handle(self, handler_input):
return ask_utils.is_intent_name("SearchEpisode")(handler_input)

def handle(self, handler_input):
player = Player(handler_input)
provider = EpisodesProvider()

search_term = ask_utils.get_slot_value(handler_input, "episode")
session_attrs = handler_input.attributes_manager.session_attributes

if len(session_attrs) == 0:
self.search_episodes(handler_input, player, provider, search_term, session_attrs)
else:
self.filter_session_episodes(handler_input, player, provider, search_term, session_attrs)

return handler_input.response_builder.response

def filter_session_episodes(self, handler_input, player, provider, search_term, session_attrs):
episodes_to_seek = session_attrs["episodes_to_seek"]
if not os.path.exists("/tmp/indexepisodes"):
os.mkdir("/tmp/indexepisodes")

schema = Schema(title=(TEXT(stored=True)))
ix = create_in("/tmp/indexepisodes", schema)

writer = ix.writer()

for ep in episodes_to_seek:
writer.add_document(title=ep)

writer.commit()
title = None

with ix.searcher() as searcher:
query = QueryParser("title", ix.schema).parse(search_term)
episode_titles = searcher.search(query)

if len(episode_titles) > 0:
title = episode_titles[0]["title"]

if title is not None:
episodes = provider.search(title)
player.play(episodes[0])
else:
speak_out = "Nenhum dos episódios sugeridos fala sobre {}. "
"Qual dos episódio sugeridos anteriormente você gostaria de ouvir?"
.format(search_term)

handler_input.response_builder.speak(speak_out).ask(speak_out)

def search_episodes(self, handler_input, player, provider, search_term, session_attrs):
episodes = provider.search(search_term)
episodes_count = 0 if episodes is None else len(episodes)

print("episodes_count: {}", episodes_count)
if episodes_count == 0:
speak_out = "Não encontrei nenhum episódio sobre {}. Qual episódio você gostaria de ouvir?".format(
search_term)
handler_input.response_builder.speak(speak_out).ask(speak_out)
elif episodes_count == 1:
player.play(episodes[0])
else:
speak_out = "Encontrei {} episódios sobre {}. São eles:<break strength="strong"/>".format(
len(episodes), search_term)

session_attrs["episodes_to_seek"] = []

ix = 0
for ep in episodes:
title = ep["title"]
session_attrs["episodes_to_seek"].append(title)

ix = ix + 1
speak_out = "{} {},<break strength="medium"/>{}<break strength="strong"/>; "
.format(speak_out, ix, title)

speak_out = "{}. Qual destes episódios você quer ouvir?".format(speak_out)
handler_input.response_builder.speak(speak_out).ask(speak_out)

Para a navegação entre os episódios, podemos usar os intents nativos da Alexa, como Amazon.NextIntent (abaixo) e o Amazon.PreviousIntent, que são ativados quando solicitamos para avançar ou retroceder, respectivamente.

class NextEpisodeHandler(AbstractRequestHandler):
    def can_handle(self, handler_input):
        return ask_utils.is_intent_name("AMAZON.NextIntent")(handler_input)

    def handle(self, handler_input):
        player = Player(handler_input)

        if not player.is_playing_episode():
            speak_out = 'Não posso iniciar o próximo episódio, pois nenhum episódio está sendo reproduzido no momento. ' 
                        'Para mais informações, basta me pedir ajuda. Qual episódio você gostaria de ouvir?'
            handler_input.response_builder.speak(speak_out).ask(speak_out)
        else:
            if not player.next():
                speak_out = 'Chegamos ao final da playlist, continue ouvindo este ou selecione outro episódio'
                handler_input.response_builder.speak(speak_out)

        return handler_input.response_builder.response

Pronto!

No próximo post, será demonstrada a gestão do estado do player, e também como os eventos do AudioPlayer são manipulados.

Para quem ainda não testou a nossa skill, lembramos que ela está disponível no Marketplace da Alexa. Gostaríamos muito de conhecer sua opinião.

Compartilhe este insight:

Comentários

Participe deixando seu comentário sobre este artigo a seguir:

Subscribe
Notify of
guest
0 Comentários
Inline Feedbacks
View all comments

AUTOR

Douglas Picolotto
Com mais de 15 anos de experiência, atua como engenheiro de nuvem e arquiteto de software, sendo especialista em Containers e DevOps. Auxilia empresas na adoção de nuvem, entregando software com maior qualidade e confiabilidade.

NOVOS HORIZONTES PARA O SEU NEGÓCIO

Nosso time está preparado para superar junto com você grandes desafios tecnológicos.

Entre em contato e vamos juntos utilizar a tecnologia do jeito certo para gerar mais resultados.

Insights EximiaCo

Confira os conteúdos de negócios e tecnologia desenvolvidos pelos nossos consultores:

Arquivo

Pós-pandemia, trabalho remoto e a retenção dos profissionais de TI

CTO Consulting e Especialista em Execução em TI
0
Queremos saber a sua opinião, deixe seu comentáriox

A sua inscrição foi realizada com sucesso!

O link de acesso à live foi enviado para o seu e-mail. Nos vemos no dia da live.

Muito obrigado!

Deu tudo certo com seu envio!
Logo entraremos em contato

Reproduzindo episódios de um podcast em um skill Alexa

Para se candidatar nesta turma aberta, preencha o formulário a seguir:

Reproduzindo episódios de um podcast em um skill Alexa

Para se candidatar nesta turma aberta, preencha o formulário a seguir:

Condição especial de pré-venda: R$ 14.000,00 - contratando a mentoria até até 31/01/2023 e R$ 15.000,00 - contratando a mentoria a partir de 01/02/2023, em até 12x com taxas.

Tenho interesse nessa capacitação

Para solicitar mais informações sobre essa capacitação para a sua empresa, preencha o formulário a seguir:

Tenho interesse em conversar

Se você está querendo gerar resultados através da tecnologia, preencha este formulário que um de nossos consultores entrará em contato com você:

O seu insight foi excluído com sucesso!

O seu insight foi excluído e não está mais disponível.

O seu insight foi salvo com sucesso!

Ele está na fila de espera, aguardando ser revisado para ter sua publicação programada.

Tenho interesse em conversar

Se você está querendo gerar resultados através da tecnologia, preencha este formulário que um de nossos consultores entrará em contato com você:

Tenho interesse nessa solução

Se você está procurando este tipo de solução para o seu negócio, preencha este formulário que um de nossos consultores entrará em contato com você:

Tenho interesse neste serviço

Se você está procurando este tipo de solução para o seu negócio, preencha este formulário que um de nossos consultores entrará em contato com você:

× Precisa de ajuda?