Como Construir uma Skill Multimodal

Como criar mais elementos visuais com menos

Índice

Já que temos um documento funcionando para a solicitação de inicialização sem contexto, vamos fazer as outras telas. Nesta seção, você completará os elementos visuais das telas de contagem regressiva e de aniversário, enquanto aprende como criar um layout reutilizável e um pacote APL. Além disso, vamos usar o componente de vídeo para termos um vídeo especial no seu aniversário.

1. Layouts

Até agora, criamos esta tela:

final no context screen image

E queremos criar estas telas:

final countdown screen image
final birthday screen image

Está vendo as semelhanças? Já podemos fazer a tela de contagem regressiva com o documento atual. E já quase conseguimos fazer a tela de aniversário. Só há duas diferenças entre a tela de aniversário e nossa atual tela de inicialização. A tela de aniversário tem um vídeo em vez de uma imagem, e o ativo de fundo é diferente. Vamos começar otimizando o documento atual com os layouts.

Os layouts são componentes compostos, ou seja, são componentes formados por outros componentes e layouts. Já vínhamos usando layouts (os componentes responsivos são layouts definidos pela Amazon no pacote APL “alexa-Layouts”), mas agora é hora de definir nosso próprio layout com base nos padrões que vemos nas telas.

Os layouts também são definidos no JSON. Eles têm uma descrição, parâmetros aceitos e uma seção de itens formada por outros componentes. Os parâmetros podem ter valores padrão se um valor parâmetro não for fornecido, além de um tipo específico (ou “qualquer”). Podemos extrair partes do texto do documento e criar um layout a partir daí. Vamos fazer isso agora.

A. Abra a authoring tool (ferramenta de criação) da APL.

B. Adicione os  layouts dos padrões repetidos na seção layouts do documento. O layout de texto reutilizável fica assim:

"cakeTimeText": {
    "description": "A basic text layout with start, middle and end.",
    "parameters":[
        {
            "name": "startText",
            "type": "string"
        },
        {
            "name": "middleText",
            "type": "string"
        },
        {
            "name": "endText",
            "type": "string"
        }
    ],
    "items": [
        {
            "type": "Container",
            "items": [
                {
                    "type": "Text",
                    "style": "bigText",
                    "text": "${startText}"
                },
                {
                    "type": "Text",
                    "style": "bigText",
                    "text": "${middleText}"
                },
                {
                    "type": "Text",
                    "style": "bigText",
                    "text": "${endText}"
                }
            ]
        }
    ]
}

Isso pode simplificar o documento existente, nos livrando de um JSON duplicado. Perceba que há uma diferença aqui entre os componentes de texto do Echo Spot e dos demais dispositivos. Resolveremos isso com uma instrução condicional dentro da propriedade paddingTop e não no contêiner. Isso vai simplificar muito o documento JSON!

C. Remova o código redundante (mova para o layout de cada seção tudo que for duplicado no código) e substitua-o pelo novo layout “cakeTimeText”. Usamos esse layout assim:

{
    "type": "cakeTimeText",
    "startText":"${text.start}",
    "middleText":"${text.middle}",
    "endText":"${text.end}"
}

Ao removermos o código redundante, substituindo-o pelas referências ao layout, temos:

Copied to clipboard
{
   "type": "APL",
   "version": "1.1",
   "settings": {},
   "theme": "dark",
   "import": [
       {
           "name": "alexa-layouts",
           "version": "1.1.0"
       }
   ],
   "resources": [],
   "styles": {
       "bigText": {
           "values": [
               {
                   "fontSize": "72dp",
                   "color": "black",
                   "textAlign": "center"
               }
           ]
       }
   },
   "onMount": [],
   "graphics": {},
   "commands": {},
   "layouts": {
       "cakeTimeText": {
           "description": "A basic text layout with start, middle and end.",
           "parameters":[
               {
                   "name": "startText",
                   "type": "string"
               },
               {
                   "name": "middleText",
                   "type": "string"
               },
               {
                   "name": "endText",
                   "type": "string"
               }
           ],
           "items": [
               {
                   "type": "Container",
                   "items": [
                       {
                           "type": "Text",
                           "style": "bigText",
                           "paddingTop":"${@viewportProfile == @hubRoundSmall ? 75dp : 0dp}",
                           "text": "${startText}"
                       },
                       {
                           "type": "Text",
                           "style": "bigText",
                           "text": "${middleText}"
                       },
                       {
                           "type": "Text",
                           "style": "bigText",
                           "text": "${endText}"
                       }
                   ]
               }
           ]
       }
   },
   "mainTemplate": {
       "parameters": [
           "text",
           "assets"
       ],
       "items": [
           {
               "type": "Container",
               "items": [
                   {
                       "type": "AlexaBackground",
                       "backgroundImageSource": "${assets.backgroundURL}"
                   },
                   {
                       "type": "cakeTimeText",
                       "startText":"${text.start}",
                       "middleText":"${text.middle}",
                       "endText":"${text.end}"
                   },
                   {
                       "type": "AlexaImage",
                       "alignSelf": "center",
                       "imageSource": "${assets.cake}",
                       "imageRoundedCorner": false,
                       "imageScale": "best-fill",
                       "imageHeight":"40vh",
                       "imageAspectRatio": "square",
                       "imageBlurredBackground": false
                   }
               ],
               "height": "100%",
               "width": "100%",
               "when": "${@viewportProfile != @hubRoundSmall}"
           },
           {
               "type": "Container",
               "items": [
                   {
                       "type": "AlexaBackground",
                       "backgroundImageSource": "${assets.backgroundURL}"
                   },
                   {
                       "type": "cakeTimeText",
                       "startText":"${text.start}",
                       "middleText":"${text.middle}",
                       "endText":"${text.end}"
                   }
               ],
               "height": "100%",
               "width": "100%",
               "when": "${@viewportProfile == @hubRoundSmall}"
           }
       ]
   }
}

Espere aí... Isso nem foi tão mais fácil assim! E parece maior agora! Vamos simplificar ainda mais o documento dividindo o layout e os estilos no próprio documento.

2. Hospede seu pacote APL

Depois de deixar os componentes reproduzindo e com a tela de inicialização permanecendo igual, é hora de hospedar o layout para que ele possa ser usado em mais de um documento. Os layouts, estilos e recursos podem ser todos hospedados em um pacote APL. Na verdade, um pacote APL tem o mesmo formato de um documento APL, mas sem mainTemplate. Essa é uma ótima maneira de compartilhar recursos, estilos, seus próprios componentes responsivos ou padrões de interface de usuário (UI) por diversos documentos APL. Você pode até criar seus próprios documentos para compartilhar com a comunidade de desenvolvedores Alexa!

Queremos hospedar seu estilo e seu layout. Para isso, usaremos nosso bucket S3 no back-end. Infelizmente, como estamos usando o ambiente hospedado na Alexa, não podemos modificar as permissões que recebemos da provisão do S3. Os dispositivos Alexa e os simuladores precisam que o cabeçalho, Access-Control-Allow-Origin seja definido e que *.amazon.com seja permitido. Para aprender mais sobre compartilhamento de recursos de origem cruzada (CORS), confira a documentação técnica. Além disso, o link deve ser público, o que não é possível com a hospedagem Alexa. Mas para este exercício, vamos usar este link do GitHub para hospedar nosso pacote APL JSON. Observação: O GitHub suporta CORS em todos os domínios.

Se você está usando outro serviço para hospedar seus ativos, esse serviço também deve enviar os cabeçalhos adequados.

Nosso pacote vai ser formado apenas pelo conjunto reutilizável de propriedades do documento APL, o que inclui os layouts e estilos. Também precisaremos da seção de importação, pois dependemos do alexa-layouts para exibir o layout. As importações são transitivas na APL. Ou seja, basicamente tudo, exceto o  mainTemplate. Nosso pacote será:

{
    "type": "APL",
    "version": "1.1",
    "settings": {},
    "theme": "dark",
    "import": [
        {
            "name": "alexa-layouts",
            "version": "1.1.0"
        }
    ],
    "resources": [],
    "styles": {
        "bigText": {
            "values": [
                {
                    "fontSize": "72dp",
                    "color": "black",
                    "textAlign": "center"
                }
            ]
        }
    },
    "onMount": [],
    "graphics": {},
    "commands": {},
    "layouts": {
        "cakeTimeText": {
            "description": "A basic text layout with start, middle and end.",
            "parameters":[
                {
                    "name": "startText",
                    "type": "string"
                },
                {
                    "name": "middleText",
                    "type": "string"
                },
                {
                    "name": "endText",
                    "type": "string"
                }
            ],
            "items": [
                {
                    "type": "Container",
                    "items": [
                        {
                            "type": "Text",
                            "style": "bigText",
                            "text": "${startText}"
                        },
                        {
                            "type": "Text",
                            "style": "bigText",
                            "text": "${middleText}"
                        },
                        {
                            "type": "Text",
                            "style": "bigText",
                            "text": "${endText}"
                        }
                    ]
                }
            ]
        }
    }
}

No documento principal, agora podemos remover tudo, exceto a parte do mainTemplate, e adicionar uma nova importação para o pacote. Na authoring tool, podemos usar este link público para testar.

A. Importe isto para o documento:

{
    "name": "my-caketime-apl-package",
    "version": "1.0",
    "source": "https://raw.githubusercontent.com/alexa/skill-sample-nodejs-first-apl-skill/master/modules/code/module4/documents/my-caketime-apl-package.json"
}

Com essa importação, podemos referenciar os valores do estilo personalizado (bigText) e do layout (cakeTimeText). O documento agora está bem menor e mais fácil de ler, já que removemos o layout e o estilo:

Copied to clipboard
{
   "type": "APL",
   "version": "1.1",
   "settings": {},
   "theme": "dark",
   "import": [
       {
           "name": "my-caketime-apl-package",
           "version": "1.0",
           "source": "https://raw.githubusercontent.com/alexa/skill-sample-nodejs-first-apl-skill/master/modules/code/module4/documents/my-caketime-apl-package.json"
       },
       {
           "name": "alexa-layouts",
           "version": "1.1.0"
       }
   ],
   "resources": [],
   "styles": {},
   "onMount": [],
   "graphics": {},
   "commands": {},
   "layouts": {},
   "mainTemplate": {
       "parameters": [
           "text",
           "assets"
       ],
       "items": [
           {
               "type": "Container",
               "items": [
                   {
                       "type": "AlexaBackground",
                       "backgroundImageSource": "${assets.backgroundURL}"
                   },
                   {
                       "type": "cakeTimeText",
                       "startText":"${text.start}",
                       "middleText":"${text.middle}",
                       "endText":"${text.end}"
                   },
                   {
                       "type": "AlexaImage",
                       "alignSelf": "center",
                       "imageSource": "${assets.cake}",
                       "imageRoundedCorner": false,
                       "imageScale": "best-fill",
                       "imageHeight":"40vh",
                       "imageAspectRatio": "square",
                       "imageBlurredBackground": false
                   }
               ],
               "height": "100%",
               "width": "100%",
               "when": "${@viewportProfile != @hubRoundSmall}"
           },
           {
               "type": "Container",
               "paddingTop": "75dp",
               "items": [
                   {
                       "type": "AlexaBackground",
                       "backgroundImageSource": "${assets.backgroundURL}"
                   },
                   {
                       "type": "cakeTimeText",
                       "startText":"${text.start}",
                       "middleText":"${text.middle}",
                       "endText":"${text.end}"
                   }
               ],
               "height": "100%",
               "width": "100%",
               "when": "${@viewportProfile == @hubRoundSmall}"
           }
       ]
   }
}

Por que ainda estamos importando alexa-layouts se as importações são transitivas? Em geral, é melhor declarar explicitamente as dependências que estiver usando diretamente. Se o pacote APL caketime decidir não usar mais alexa-layouts, seu documento vai quebrar! Portanto, seu documento também tem uma dependência no alexa-layouts.

B. Abra o Portal dos Desenvolvedor para sua skill de Cake Time.

C. Salve o novo documento sobre o launchDocument.json atual, na aba Code (código) da skill.

Agora, vamos fazer o documento final.

3. Adicione um vídeo especial de aniversário

Nosso birthdayDocument vai usar um vídeo em tela cheia em vez do componente de imagem com o texto removido, fornecendo o mesmo layout. O componente de vídeo tem uma estrutura simples para o nosso caso de uso.

A. Na aba Code, crie um novo documento chamado birthdayDocument.jsonuma cópia do documento antigo.

B. Substitua o componente de imagem do birthdayDocument.json pelo seguinte componente de vídeo, dentro de um contêiner:

{
    "type": "Container",
    "paddingTop":"3vh",
    "alignItems": "center",
    "items": [{
        "type": "Video",
        "height": "85vh",
        "width":"90vw",
        "source": "${assets.video}",
        "autoplay": true
    }]
}

Com esse contêiner, podemos centralizar o componente no documento APL. Com o preenchimento como está, conseguimos ver parte do fundo, na parte superior da janela de visualização. Note que também removemos o objeto de texto neste primeiro contêiner! Queremos que o vídeo apareça na frente e centralizado. O vídeo que vamos usar é um bolo de aniversário animado, com a Alexa cantando ao fundo. Ele fica assim:

Esse vídeo foi feito para ser reproduzido em tela cheia, e é por isso que a altura é de 85%, e a altura e a largura são de 90% da janela de visualização. Porém, agora não queremos mais o texto quando exibimos o vídeo.

C. Remova o componente CakeTimeText do primeiro contêiner (when ${@viewportProfile != @hubRoundSmall}).

D. Salve no código da skill como um arquivo novo chamado birthdayDocument.json.

4. Conecte o back-end

Vamos voltar ao arquivo index.js e conectar as outras telas da APL.

A única diferença entre o documento de inicialização e o nosso conhecido documento de aniversário é o conteúdo. Vamos começar a modificar o  HasBirthdayLaunchRequestHandler para, condicionalmente, usar o arquivo launchDocument.json ou o birthdayDocument.json, dependendo da situação. A ideia é que fique assim:

final countdown screen image

A. Devemos evitar duplicar o código, então vamos fazer uma função auxiliar para pegar a imagem de fundo com base na chave. Usaremos a mesma coisa para buscar uma imagem de fundo para o documento alternativo. Além disso, ela também será usada para limitar o tamanho da tela do dispositivo à lógica do tamanho do ativo. Adicione a função auxiliar ao index.js em qualquer lugar fora de outra função ou objeto:

function getBackgroundURL(handlerInput, fileNamePrefix) {
    const viewportProfile = Alexa.getViewportProfile(handlerInput.requestEnvelope);
    const backgroundKey = viewportProfile === 'TV-LANDSCAPE-XLARGE' ? "Media/"+fileNamePrefix+"_1920x1080.png" : "Media/"+fileNamePrefix+"_1280x800.png";
    return util.getS3PreSignedUrl(backgroundKey);
}

A vantagem disso é ter um lugar único para guardar as suposições (assumptions) de nomes de arquivos. Se quisermos adicionar mais detecções de perfil de janela de visualização ou se decidirmos tirar a hospedagem do bucket S3, estará tudo num lugar só.

B. Precisamos refatorar o código LaunchRequestHandler.handle() para usar a nova função. A nova fonte de dados agora terá um novo valor para a chave backgroundURL:

backgroundURL: getBackgroundURL(handlerInput, "lights")

E você pode excluir as linhas:

const viewportProfile = Alexa.getViewportProfile(handlerInput.requestEnvelope);
const backgroundKey = viewportProfile === 'TV-LANDSCAPE-XLARGE' ? "Media/lights_1920x1080.png" : "Media/lights_1280x800.png";

C. Como estamos usando o mesmo documento de lançamento, já temos a importação para o JSON representando o documento. Adicione um bloco ao HasBirthdayLaunchRequestHandlerparecido com o LaunchRequestHandler, imediatamente antes da instrução de retorno.

// Add APL directive to response
if (Alexa.getSupportedInterfaces(handlerInput.requestEnvelope)['Alexa.Presentation.APL']) {
    // Create Render Directive
}

D. Vamos definir uma variável para usar na fonte de dados, chamada  numberDaysString. Esta é uma string variável que ficará como “1 dia” ou “234 dias”. Você pode representá-la com a expressão:

const numberDaysString = diffDays === 1 ? "1 day": diffDays + " days";

Inclua essa variável logo abaixo do comentário  // Add APL directive to response.

E. Agora, sob // Create Render Directive, adicione a diretiva:

handlerInput.responseBuilder.addDirective({
    type: 'Alexa.Presentation.APL.RenderDocument',
    document: launchDocument,
    datasources: {
        text: {
            type: 'object',
            start: "Your Birthday",
            middle: "is in",
            end: numberDaysString
        },
        assets: {
            cake: util.getS3PreSignedUrl('Media/alexaCake_960x960.png'),
            backgroundURL: getBackgroundURL(handlerInput, "lights")
        }
    }
});

Note que estamos usando a numberDaysString na fonte de dados, então haverá mudanças com base nas entradas e no dia do ano. Além disso, estamos usando a função auxiliar para construir a URL das luzes para buscar a URL assinada adequada.

F. Teste! Você precisará passar pelo fluxo inteiro para informar o mês, dia e ano do seu nascimento antes de ver essa tela.

5. Conecte o vídeo de aniversário

A. Depois de verificar que está tudo funcionando, vamos construir outro caminho para quando for seu aniversário. Ele usará nosso novo documento birthdayDocument.json, então vamos começar importando-o como birthdayDocument, no topo.

const birthdayDocument = require('./documents/birthdayDocument.json');

B. Agora precisamos adicionar uma lógica condicional ao código, para alternar entre documentos APL (dependendo se é ou não seu aniversário). Sob o comentário // Create Render Directive, dentro do método HasBirthdayLaunchRequestHandler, adicione:

if (currentDate.getTime() !== nextBirthday) {
    //TODO Move the old directive here.
} else {
    //TODO Write a birthday specific directive here.
}

C. Recorte o handlerInput.responseBuilder.addDirectiveReplace({…​}) que você adicionou na última seção e substitua o comentário, //TODO Move the old directive here. por isso.

D. Dentro bloco else podemos adicionar uma nova diretiva usando o birthdayDocument que você importou acima. Usaremos a foto  "confetti" (confetes). Adicione esta diretiva inteira no bloco else:

// Create Render Directive
handlerInput.responseBuilder.addDirective({
    type: 'Alexa.Presentation.APL.RenderDocument',
    document: birthdayDocument,
    datasources: {
        text: {
            type: 'object',
            start: "Happy Birthday!",
            middle: "From,",
            end: "Alexa <3"
        },
        assets: {
            video: "https://public-pics-muoio.s3.amazonaws.com/video/Amazon_Cake.mp4",
            backgroundURL: getBackgroundURL(handlerInput, "confetti")
        }
    }
});

Esta nova diretiva é diferente no que se refere aos dados fornecidos pelo objeto de texto, à imagem substituída por um vídeo, e ao uso do ativo de confete pelo fundo. Ainda precisamos incluir um texto de começo, meio e fim porque nossa variante para o Hub Pequeno (Redondo) usa isso.

E. Agora teste a skill. Limpe seus dados de usuário no S3 e minta dizendo que hoje é seu aniversário! Se não estiver mentindo, então... Feliz Aniversário! :)

Quando tudo estiver funcionando, você poderá ir para o último módulo para aprender sobre os comandos.

Código completo no GitHub