Como fazer um editor de síntese de fala
Publicados: 2022-03-10Quando Steve Jobs apresentou o Macintosh em 1984, ele nos disse “Olá” do palco. Mesmo naquela época, a síntese de voz não era realmente uma tecnologia nova: a Bell Labs desenvolveu o vocoder no final dos anos 30, e o conceito de um computador assistente de voz tornou-se conhecido das pessoas quando Stanley Kubrick fez do vocoder a voz de HAL9000 em 2001: Uma Odisseia no Espaço (1968).
Não foi antes da introdução do Siri, Amazon Echo e Google Assistant da Apple em meados de 2015 que as interfaces de voz realmente chegaram às casas, pulsos e bolsos de um público mais amplo. Ainda estamos em fase de adoção, mas parece que esses assistentes de voz vieram para ficar.
Em outras palavras, a web não é mais apenas texto passivo em uma tela . Editores da Web e designers de UX precisam se acostumar a criar conteúdo e serviços que devem ser falados em voz alta.
Já estamos avançando rapidamente para o uso de sistemas de gerenciamento de conteúdo que nos permitem trabalhar com nosso conteúdo sem controle e por meio de APIs. A parte final é fazer interfaces editoriais que facilitem a adaptação do conteúdo para voz. Então vamos fazer exatamente isso!
O que é SSML
Enquanto os navegadores da Web usam a especificação do W3C para HyperText Markup Language (HTML) para renderizar documentos visualmente, a maioria dos assistentes de voz usa Speech Synthesis Markup Language (SSML) ao gerar fala.
Um exemplo mínimo usando o elemento raiz <speak>
e as tags de parágrafo ( <p>
) e frase ( <s>
):
<speak> <p> <s>This is the first sentence of the paragraph.</s> <s>Here's another sentence.</s> </p> </speak>
Onde o SSML se torna existente é quando introduzimos tags para <emphasis>
e <prosody>
(pitch):
<speak> <p> <s>Put some <emphasis strength="strong">extra weight on these words</emphasis></s> <s>And say <prosody pitch="high" rate="fast">this a bit higher and faster</prosody>!</s> </p> </speak>
O SSML tem mais recursos, mas isso é suficiente para ter uma ideia do básico. Agora, vamos dar uma olhada no editor que usaremos para fazer a interface de edição da síntese de fala.
O Editor de Texto Portátil
Para fazer este editor, usaremos o editor de Portable Text que está disponível no Sanity.io. Portable Text é uma especificação JSON para edição de rich text, que pode ser serializada em qualquer linguagem de marcação, como SSML. Isso significa que você pode usar facilmente o mesmo snippet de texto em vários lugares usando diferentes linguagens de marcação.
Instalando o Sanity
Sanity.io é uma plataforma para conteúdo estruturado que vem com um ambiente de edição de código aberto construído com React.js. Leva dois minutos para colocar tudo em funcionamento.
Digite npm i -g @sanity/cli && sanity init
em seu terminal e siga as instruções. Escolha “vazio”, quando for solicitado um modelo de projeto.
Se você não quiser seguir este tutorial e fazer este editor do zero, você também pode clonar o código deste tutorial e seguir as instruções em README.md
.
Quando o editor é baixado, você executa o sanity start
na pasta do projeto para iniciá-lo. Ele iniciará um servidor de desenvolvimento que usa o Hot Module Reloading para atualizar as alterações à medida que você edita seus arquivos.
Como configurar esquemas no Sanity Studio
Criando os arquivos do editor
Começaremos criando uma pasta chamada ssml-editor na pasta /schemas . Nessa pasta, colocaremos alguns arquivos vazios:
/ssml-tutorial/schemas/ssml-editor ├── alias.js ├── emphasis.js ├── annotations.js ├── preview.js ├── prosody.js ├── sayAs.js ├── blocksToSSML.js ├── speech.js ├── SSMLeditor.css └── SSMLeditor.js
Agora podemos adicionar esquemas de conteúdo nesses arquivos. Os esquemas de conteúdo são o que define a estrutura de dados para o rich text e o que o Sanity Studio usa para gerar a interface editorial. Eles são objetos JavaScript simples que geralmente requerem apenas um name
e um type
.
Também podemos adicionar um title
e uma description
para tornar um pouco melhor para os editores. Por exemplo, este é um esquema para um campo de texto simples para um title
:
export default { name: 'title', type: 'string', title: 'Title', description: 'Titles should be short and descriptive' }
O Portable Text é construído com base na ideia de rich text como dados. Isso é poderoso porque permite consultar seu rich text e convertê-lo em praticamente qualquer marcação desejada.
É uma matriz de objetos chamados “blocos” que você pode pensar como os “parágrafos”. Em um bloco, há uma matriz de spans filhos. Cada bloco pode ter um estilo e um conjunto de definições de marca, que descrevem as estruturas de dados distribuídas nos spans filhos.
Sanity.io vem com um editor que pode ler e escrever em Portable Text, e é ativado colocando o tipo de block
dentro de um campo de array
, assim:
// speech.js export default { name: 'speech', type: 'array', title: 'SSML Editor', of: [ { type: 'block' } ] }
Uma matriz pode ser de vários tipos. Para um editor SSML, esses podem ser blocos para arquivos de áudio, mas isso está fora do escopo deste tutorial.
A última coisa que queremos fazer é adicionar um tipo de conteúdo onde este editor possa ser usado. A maioria dos assistentes usa um modelo de conteúdo simples de “intenções” e “cumprimentos”:
- Intenções
Normalmente, uma lista de strings usadas pelo modelo de IA para delinear o que o usuário deseja fazer. - Cumprimentos
Isso acontece quando uma “intenção” é identificada. Um cumprimento geralmente é – ou pelo menos – vem com algum tipo de resposta.
Então vamos fazer um tipo de conteúdo simples chamado fulfillment
que usa o editor de síntese de fala. Crie um novo arquivo chamado fulfillment.js e salve-o na pasta /schema :
// fulfillment.js export default { name: 'fulfillment', type: 'document', title: 'Fulfillment', of: [ { name: 'title', type: 'string', title: 'Title', description: 'Titles should be short and descriptive' }, { name: 'response', type: 'speech' } ] }
Salve o arquivo e abra o schema.js . Adicione-o ao seu estúdio assim:
// schema.js import createSchema from 'part:@sanity/base/schema-creator' import schemaTypes from 'all:part:@sanity/base/schema-type' import fullfillment from './fullfillment' import speech from './speech' export default createSchema({ name: 'default', types: schemaTypes.concat([ fullfillment, speech, ]) })
Se você agora executar sanity start
em sua interface de linha de comando dentro da pasta raiz do projeto, o estúdio será inicializado localmente e você poderá adicionar entradas para fulfillments. Você pode manter o estúdio funcionando enquanto continuamos, pois ele será recarregado automaticamente com novas alterações quando você salvar os arquivos.
Adicionando SSML ao Editor
Por padrão, o tipo de block
fornecerá um editor padrão para rich text orientado visualmente com estilos de cabeçalho, estilos de decorador para ênfase e força, anotações para links e listas. Agora queremos substituir aqueles com os conceitos auditivos encontrados no SSML.
Começamos definindo as diferentes estruturas de conteúdo, com descrições úteis para os editores, que adicionaremos ao block
em SSMLeditorSchema.js como configurações para annotations
. Esses são “ênfase”, “alias”, “prosódia” e “digamos como”.
Ênfase
Começamos com “ênfase”, que controla quanto peso é colocado no texto marcado. Definimos como uma string com uma lista de valores predefinidos que o usuário pode escolher:
// emphasis.js export default { name: 'emphasis', type: 'object', title: 'Emphasis', description: 'The strength of the emphasis put on the contained text', fields: [ { name: 'level', type: 'string', options: { list: [ { value: 'strong', title: 'Strong' }, { value: 'moderate', title: 'Moderate' }, { value: 'none', title: 'None' }, { value: 'reduced', title: 'Reduced' } ] } } ] }
Alias
Às vezes, o termo escrito e o falado diferem. Por exemplo, você deseja usar a abreviação de uma frase em um texto escrito, mas a frase inteira é lida em voz alta. Por exemplo:
<s>This is a <sub alias="Speech Synthesis Markup Language">SSML</sub> tutorial</s>
O campo de entrada para o alias é uma string simples:
// alias.js export default { name: 'alias', type: 'object', title: 'Alias (sub)', description: 'Replaces the contained text for pronunciation. This allows a document to contain both a spoken and written form.', fields: [ { name: 'text', type: 'string', title: 'Replacement text', } ] }
Prosódia
Com a propriedade prosódia podemos controlar diferentes aspectos de como o texto deve ser falado, como tom, velocidade e volume. A marcação para isso pode ser assim:
<s>Say this with an <prosody pitch="x-low">extra low pitch</prosody>, and this <prosody rate="fast" volume="loud">loudly with a fast rate</prosody></s>
Esta entrada terá três campos com opções de strings predefinidas:
// prosody.js export default { name: 'prosody', type: 'object', title: 'Prosody', description: 'Control of the pitch, speaking rate, and volume', fields: [ { name: 'pitch', type: 'string', title: 'Pitch', description: 'The baseline pitch for the contained text', options: { list: [ { value: 'x-low', title: 'Extra low' }, { value: 'low', title: 'Low' }, { value: 'medium', title: 'Medium' }, { value: 'high', title: 'High' }, { value: 'x-high', title: 'Extra high' }, { value: 'default', title: 'Default' } ] } }, { name: 'rate', type: 'string', title: 'Rate', description: 'A change in the speaking rate for the contained text', options: { list: [ { value: 'x-slow', title: 'Extra slow' }, { value: 'slow', title: 'Slow' }, { value: 'medium', title: 'Medium' }, { value: 'fast', title: 'Fast' }, { value: 'x-fast', title: 'Extra fast' }, { value: 'default', title: 'Default' } ] } }, { name: 'volume', type: 'string', title: 'Volume', description: 'The volume for the contained text.', options: { list: [ { value: 'silent', title: 'Silent' }, { value: 'x-soft', title: 'Extra soft' }, { value: 'medium', title: 'Medium' }, { value: 'loud', title: 'Loud' }, { value: 'x-loud', title: 'Extra loud' }, { value: 'default', title: 'Default' } ] } } ] }
Diga como
O último que queremos incluir é <say-as>
. Essa tag nos permite exercer um pouco mais de controle sobre como certas informações são pronunciadas. Podemos até usá-lo para emitir palavras se você precisar redigir algo nas interfaces de voz. Isso é @!%& útil!
<s>Do I have to <say-as interpret-as="expletive">frakking</say-as> <say-as interpret-as="verbatim">spell</say-as> it out for you!?</s>
// sayAs.js export default { name: 'sayAs', type: 'object', title: 'Say as...', description: 'Lets you indicate information about the type of text construct that is contained within the element. It also helps specify the level of detail for rendering the contained text.', fields: [ { name: 'interpretAs', type: 'string', title: 'Interpret as...', options: { list: [ { value: 'cardinal', title: 'Cardinal numbers' }, { value: 'ordinal', title: 'Ordinal numbers (1st, 2nd, 3th...)' }, { value: 'characters', title: 'Spell out characters' }, { value: 'fraction', title: 'Say numbers as fractions' }, { value: 'expletive', title: 'Blip out this word' }, { value: 'unit', title: 'Adapt unit to singular or plural' }, { value: 'verbatim', title: 'Spell out letter by letter (verbatim)' }, { value: 'date', title: 'Say as a date' }, { value: 'telephone', title: 'Say as a telephone number' } ] } }, { name: 'date', type: 'object', title: 'Date', fields: [ { name: 'format', type: 'string', description: 'The format attribute is a sequence of date field character codes. Supported field character codes in format are {y, m, d} for year, month, and day (of the month) respectively. If the field code appears once for year, month, or day then the number of digits expected are 4, 2, and 2 respectively. If the field code is repeated then the number of expected digits is the number of times the code is repeated. Fields in the date text may be separated by punctuation and/or spaces.' }, { name: 'detail', type: 'number', validation: Rule => Rule.required() .min(0) .max(2), description: 'The detail attribute controls the spoken form of the date. For detail='1' only the day fields and one of month or year fields are required, although both may be supplied' } ] } ] }
Agora podemos importá-los em um arquivo annotations.js , o que torna as coisas um pouco mais organizadas.
// annotations.js export {default as alias} from './alias' export {default as emphasis} from './emphasis' export {default as prosody} from './prosody' export {default as sayAs} from './sayAs'
Agora podemos importar esses tipos de anotação para nossos esquemas principais:
// schema.js import createSchema from "part:@sanity/base/schema-creator" import schemaTypes from "all:part:@sanity/base/schema-type" import fulfillment from './fulfillment' import speech from './ssml-editor/speech' import { alias, emphasis, prosody, sayAs } from './annotations' export default createSchema({ name: "default", types: schemaTypes.concat([ fulfillment, speech, alias, emphasis, prosody, sayAs ]) })
Finalmente, agora podemos adicioná-los ao editor assim:
// speech.js export default { name: 'speech', type: 'array', title: 'SSML Editor', of: [ { type: 'block', styles: [], lists: [], marks: { decorators: [], annotations: [ {type: 'alias'}, {type: 'emphasis'}, {type: 'prosody'}, {type: 'sayAs'} ] } } ] }
Observe que também adicionamos arrays vazios a styles
e decorators
. Isso desativa os estilos e decoradores padrão (como negrito e ênfase), pois eles não fazem muito sentido neste caso específico.
Personalizando a aparência
Agora temos a funcionalidade no lugar, mas como não especificamos nenhum ícone, cada anotação usará o ícone padrão, o que dificulta o uso do editor pelos autores. Então vamos corrigir isso!
Com o editor para Portable Text é possível injetar componentes React tanto para os ícones quanto para como o texto marcado deve ser renderizado. Aqui, vamos deixar alguns emojis fazerem o trabalho para nós, mas você obviamente pode ir longe com isso, tornando-os dinâmicos e assim por diante. Para prosody
, faremos até que o ícone mude dependendo do volume selecionado. Observe que omiti os campos nesses trechos por brevidade, você não deve removê-los em seus arquivos locais.
// alias.js import React from 'react' export default { name: 'alias', type: 'object', title: 'Alias (sub)', description: 'Replaces the contained text for pronunciation. This allows a document to contain both a spoken and written form.', fields: [ /* all the fields */ ], blockEditor: { icon: () => '', render: ({ children }) => <span>{children} </span>, }, };
// emphasis.js import React from 'react' export default { name: 'emphasis', type: 'object', title: 'Emphasis', description: 'The strength of the emphasis put on the contained text', fields: [ /* all the fields */ ], blockEditor: { icon: () => '', render: ({ children }) => <span>{children} </span>, }, };
// prosody.js import React from 'react' export default { name: 'prosody', type: 'object', title: 'Prosody', description: 'Control of the pitch, speaking rate, and volume', fields: [ /* all the fields */ ], blockEditor: { icon: () => '', render: ({ children, volume }) => ( <span> {children} {['x-loud', 'loud'].includes(volume) ? '' : ''} </span> ), }, };
// sayAs.js import React from 'react' export default { name: 'sayAs', type: 'object', title: 'Say as...', description: 'Lets you indicate information about the type of text construct that is contained within the element. It also helps specify the level of detail for rendering the contained text.', fields: [ /* all the fields */ ], blockEditor: { icon: () => '', render: props => <span>{props.children} </span>, }, };
Agora você tem um editor para edição de texto que pode ser usado por assistentes de voz. Mas não seria útil se os editores também pudessem visualizar como o texto realmente soará?
Adicionando um botão de visualização usando o Text-to-Speech do Google
O suporte nativo à síntese de fala está a caminho para os navegadores. Mas neste tutorial, usaremos a API Text-to-Speech do Google que suporta SSML. A criação dessa funcionalidade de visualização também será uma demonstração de como você serializa o Portable Text em SSML em qualquer serviço para o qual deseja usá-lo.
Envolvendo o editor em um componente React
Começamos abrindo o arquivo SSMLeditor.js e adicionamos o seguinte código:
// SSMLeditor.js import React, { Fragment } from 'react'; import { BlockEditor } from 'part:@sanity/form-builder'; export default function SSMLeditor(props) { return ( <Fragment> <BlockEditor {...props} /> </Fragment> ); }
Agora envolvemos o editor em nosso próprio componente React. Todos os adereços necessários, incluindo os dados que contém, são transmitidos em tempo real. Para realmente usar este componente, você precisa importá-lo para o seu arquivo speech.js
:
// speech.js import React from 'react' import SSMLeditor from './SSMLeditor.js' export default { name: 'speech', type: 'array', title: 'SSML Editor', inputComponent: SSMLeditor, of: [ { type: 'block', styles: [], lists: [], marks: { decorators: [], annotations: [ { type: 'alias' }, { type: 'emphasis' }, { type: 'prosody' }, { type: 'sayAs' }, ], }, }, ], }
Quando você salva isso e o estúdio recarrega, deve parecer exatamente o mesmo, mas isso é porque ainda não começamos a ajustar o editor.
Converter texto portátil em SSML
O editor salvará o conteúdo como Portable Text, uma matriz de objetos em JSON que facilita a conversão de rich text em qualquer formato que você precisar. Quando você converte Portable Text em outra sintaxe ou formato, chamamos isso de “serialização”. Portanto, “serializadores” são as receitas de como o rich text deve ser convertido. Nesta seção, adicionaremos serializadores para síntese de fala.
Você já criou o arquivo blocksToSSML.js . Agora precisamos adicionar nossa primeira dependência. Comece executando o comando de terminal npm init -y
dentro da pasta ssml-editor
. Isso adicionará um package.json onde as dependências do editor serão listadas.
Feito isso, você pode executar npm install @sanity/block-content-to-html
para obter uma biblioteca que facilita a serialização de Portable Text. Estamos usando a biblioteca HTML porque o SSML tem a mesma sintaxe XML com tags e atributos.
Este é um monte de código, então sinta-se à vontade para copiá-lo e colá-lo. Vou explicar o padrão logo abaixo do trecho:
// blocksToSSML.js import blocksToHTML, { h } from '@sanity/block-content-to-html' const serializers = { marks: { prosody: ({ children, mark: { rate, pitch, volume } }) => h('prosody', { attrs: { rate, pitch, volume } }, children), alias: ({ children, mark: { text } }) => h('sub', { attrs: { alias: text } }, children), sayAs: ({ children, mark: { interpretAs } }) => h('say-as', { attrs: { 'interpret-as': interpretAs } }, children), break: ({ children, mark: { time, strength } }) => h('break', { attrs: { time: '${time}ms', strength } }, children), emphasis: ({ children, mark: { level } }) => h('emphasis', { attrs: { level } }, children) } } export const blocksToSSML = blocks => blocksToHTML({ blocks, serializers })
Este código exportará uma função que pega o array de blocos e faz um loop por eles. Sempre que um bloco contiver uma mark
, ele procurará um serializador para o tipo. Se você marcou algum texto para ter emphasis
, esta função do objeto serializadores:
emphasis: ({ children, mark: { level } }) => h('emphasis', { attrs: { level } }, children)
Talvez você reconheça o parâmetro de onde definimos o esquema? A função h()
nos permite definir um elemento HTML, ou seja, aqui nós “enganamos” e faz com que ele retorne um elemento SSML chamado <emphasis>
. Também damos a ele o level
de atributo, se estiver definido, e colocamos os elementos children
dentro dele — que na maioria dos casos será o texto que você marcou com emphasis
.
{ "_type": "block", "_key": "f2c4cf1ab4e0", "style": "normal", "markDefs": [ { "_type": "emphasis", "_key": "99b28ed3fa58", "level": "strong" } ], "children": [ { "_type": "span", "_key": "f2c4cf1ab4e01", "text": "Say this strongly!", "marks": [ "99b28ed3fa58" ] } ] }
É assim que a estrutura acima no Portable Text é serializada para este SSML:
<emphasis level="strong">Say this strongly</emphasis>
Se você quiser suporte para mais tags SSML, poderá adicionar mais anotações no esquema e adicionar os tipos de anotação à seção de marks
nos serializadores.
Agora temos uma função que retorna a marcação SSML do nosso rich text marcado. A última parte é fazer um botão que nos permite enviar essa marcação para um serviço de conversão de texto em fala.
Adicionando um botão de visualização que fala de volta para você
Idealmente, deveríamos ter usado os recursos de síntese de fala do navegador na API da Web. Dessa forma, teríamos conseguido menos código e dependências.
No início de 2019, no entanto, o suporte nativo do navegador para síntese de fala ainda está em seus estágios iniciais. Parece que o suporte para SSML está a caminho, e há provas de conceitos de implementações JavaScript do lado do cliente para ele.
É provável que você use esse conteúdo com um assistente de voz de qualquer maneira. Tanto o Google Assistant quanto o Amazon Echo (Alexa) oferecem suporte a SSML como respostas em um fulfillment. Neste tutorial, usaremos a API de conversão de texto em fala do Google, que também soa bem e suporta vários idiomas.
Comece obtendo uma chave de API inscrevendo-se no Google Cloud Platform (será gratuito para os primeiros 1 milhão de caracteres que você processar). Depois de se inscrever, você pode criar uma nova chave de API nesta página.
Agora você pode abrir seu arquivo PreviewButton.js e adicionar este código a ele:
// PreviewButton.js import React from 'react' import Button from 'part:@sanity/components/buttons/default' import { blocksToSSML } from './blocksToSSML' // You should be careful with sharing this key // I put it here to keep the code simple const API_KEY = '<yourAPIkey>' const GOOGLE_TEXT_TO_SPEECH_URL = 'https://texttospeech.googleapis.com/v1beta1/text:synthesize?key=' + API_KEY const speak = async blocks => { // Serialize blocks to SSML const ssml = blocksToSSML(blocks) // Prepare the Google Text-to-Speech configuration const body = JSON.stringify({ input: { ssml }, // Select the language code and voice name (AF) voice: { languageCode: 'en-US', name: 'en-US-Wavenet-A' }, // Use MP3 in order to play in browser audioConfig: { audioEncoding: 'MP3' } }) // Send the SSML string to the API const res = await fetch(GOOGLE_TEXT_TO_SPEECH_URL, { method: 'POST', body }).then(res => res.json()) // Play the returned audio with the Browser's Audo API const audio = new Audio('data:audio/wav;base64,' + res.audioContent) audio.play() } export default function PreviewButton (props) { return <Button style={{ marginTop: '1em' }} onClick={() => speak(props.blocks)}>Speak text</Button> }
Mantive este código de botão de visualização no mínimo para facilitar o acompanhamento deste tutorial. Claro, você pode construí-lo adicionando estado para mostrar se a visualização está sendo processada ou possibilitar a visualização com as diferentes vozes que a API do Google suporta.
Adicione o botão ao SSMLeditor.js
:
// SSMLeditor.js import React, { Fragment } from 'react'; import { BlockEditor } from 'part:@sanity/form-builder'; import PreviewButton from './PreviewButton'; export default function SSMLeditor(props) { return ( <Fragment> <BlockEditor {...props} /> <PreviewButton blocks={props.value} /> </Fragment> ); }
Agora você poderá marcar seu texto com as diferentes anotações e ouvir o resultado ao pressionar “Falar texto”. Legal, não é?
Você criou um editor de síntese de fala, e agora?
Se você seguiu este tutorial, aprendeu como usar o editor para Portable Text no Sanity Studio para fazer anotações personalizadas e personalizar o editor. Você pode usar essas habilidades para todo tipo de coisa, não apenas para fazer um editor de síntese de fala. Você também aprendeu como serializar o Portable Text na sintaxe de que precisa. Obviamente, isso também é útil se você estiver construindo frontends em React ou Vue. Você pode até usar essas habilidades para gerar Markdown de Texto Portátil.
Não abordamos como você realmente usa isso junto com um assistente de voz. Se você quiser tentar, pode usar a mesma lógica do botão de visualização em uma função sem servidor e defini-lo como o ponto de extremidade da API para um cumprimento usando webhooks, por exemplo, com o Dialogflow.
Se você quiser que eu escreva um tutorial sobre como usar o editor de síntese de voz com um assistente de voz, sinta-se à vontade para me dar uma dica no Twitter ou compartilhar na seção de comentários abaixo.
Leitura adicional no SmashingMag:
- Experimentando com a síntese de fala
- Aprimorando a experiência do usuário com a API Web Speech
- APIs de acessibilidade: uma chave para a acessibilidade na Web
- Criando um chatbot de IA simples com API de fala na Web e Node.js