Как сделать редактор синтеза речи
Опубликовано: 2022-03-10Когда Стив Джобс представил Macintosh в 1984 году, он сказал нам «привет» со сцены. Даже на тот момент синтез речи не был действительно новой технологией: Bell Labs разработала вокодер еще в конце 30-х годов, а концепция голосового помощника стала известна людям, когда Стэнли Кубрик превратил вокодер в голос человека. HAL9000 в 2001 году: Космическая одиссея (1968).
Только после появления Apple Siri, Amazon Echo и Google Assistant в середине 2015-х годов голосовые интерфейсы действительно нашли свое применение в домах, на запястьях и в карманах более широкой публики. Мы все еще находимся на этапе принятия, но кажется, что эти голосовые помощники останутся.
Другими словами, Интернет больше не является просто пассивным текстом на экране . Веб-редакторы и дизайнеры UX должны привыкнуть к созданию контента и услуг, о которых следует говорить вслух.
Мы уже быстро движемся к использованию систем управления контентом, которые позволяют нам работать с нашим контентом без головы и через API. Последняя часть — создание редакторских интерфейсов, упрощающих адаптацию контента для озвучивания. Так давайте сделаем именно это!
Что такое SSML
В то время как веб-браузеры используют спецификацию W3C для языка гипертекстовой разметки (HTML) для визуального отображения документов, большинство голосовых помощников используют язык разметки синтеза речи (SSML) при генерации речи.
Минимальный пример с использованием корневого элемента <speak>
и тегов абзаца ( <p>
) и предложения ( <s>
):
<speak> <p> <s>This is the first sentence of the paragraph.</s> <s>Here's another sentence.</s> </p> </speak>
SSML появляется, когда мы вводим теги для <emphasis>
и <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>
В SSML больше возможностей, но и этого достаточно, чтобы понять основы. Теперь давайте подробнее рассмотрим редактор, который мы будем использовать для создания интерфейса редактирования синтеза речи.
Редактор переносимого текста
Чтобы сделать этот редактор, мы будем использовать редактор Portable Text, который есть в Sanity.io. Portable Text — это спецификация JSON для редактирования форматированного текста, которую можно преобразовать в любой язык разметки, например SSML. Это означает, что вы можете легко использовать один и тот же фрагмент текста в нескольких местах, используя разные языки разметки.
Установка здравомыслия
Sanity.io — это платформа для структурированного контента, которая поставляется со средой редактирования с открытым исходным кодом, созданной с помощью React.js. На то, чтобы все это заработало, уходит две минуты.
Введите npm i -g @sanity/cli && sanity init
в свой терминал и следуйте инструкциям. Выберите «пусто», когда вам будет предложено ввести шаблон проекта.
Если вы не хотите следовать этому руководству и создавать этот редактор с нуля, вы также можете клонировать код этого руководства и следовать инструкциям в README.md
.
Когда редактор загружен, вы запускаете sanity start
в папке проекта, чтобы запустить его. Он запустит сервер разработки, который использует горячую перезагрузку модуля для обновления изменений при редактировании его файлов.
Как настроить схемы в Sanity Studio
Создание файлов редактора
Мы начнем с создания папки с именем ssml-editor в папке / schemas. В эту папку мы поместим несколько пустых файлов:
/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
Теперь мы можем добавить схемы контента в эти файлы. Схемы контента — это то, что определяет структуру данных для форматированного текста и то, что Sanity Studio использует для создания редакторского интерфейса. Это простые объекты JavaScript, которым обычно требуется только name
и type
.
Мы также можем добавить title
и description
, чтобы редакторам было удобнее. Например, это схема простого текстового поля для title
:
export default { name: 'title', type: 'string', title: 'Title', description: 'Titles should be short and descriptive' }
Portable Text построен на идее форматированного текста как данных. Это мощно, потому что позволяет вам запрашивать ваш форматированный текст и преобразовывать его практически в любую разметку, которую вы хотите.
Это массив объектов, называемых «блоками», которые вы можете рассматривать как «абзацы». В блоке есть массив дочерних спанов. Каждый блок может иметь стиль и набор определений меток, описывающих структуры данных, распределенные по дочерним участкам.
Sanity.io поставляется с редактором, который может читать и писать в Portable Text, и активируется помещением типа block
в поле array
, например:
// speech.js export default { name: 'speech', type: 'array', title: 'SSML Editor', of: [ { type: 'block' } ] }
Массив может быть нескольких типов. Для редактора SSML это могут быть блоки для аудиофайлов, но это выходит за рамки данного руководства.
Последнее, что нам нужно сделать, это добавить тип контента, в котором можно будет использовать этот редактор. Большинство помощников используют простую модель контента «намерения» и «исполнения»:
- Намерения
Обычно это список строк, используемых моделью ИИ для определения того, что пользователь хочет сделать. - Исполнения
Это происходит, когда определяется «намерение». Исполнение часто приходит — или, по крайней мере — приходит с каким-то ответом.
Итак, давайте создадим простой тип контента под названием « fulfillment
», который использует редактор синтеза речи. Создайте новый файл с именем fillment.js и сохраните его в папке /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' } ] }
Сохраните файл и откройте schema.js . Добавьте его в свою студию следующим образом:
// 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, ]) })
Если теперь вы запустите sanity start
в интерфейсе командной строки в корневой папке проекта, студия запустится локально, и вы сможете добавлять записи для выполнения. Вы можете оставить студию работающей, пока мы продолжаем, так как она будет автоматически перезагружаться с новыми изменениями при сохранении файлов.
Добавление SSML в редактор
По умолчанию block
тип предоставит вам стандартный редактор для визуально ориентированного форматированного текста со стилями заголовков, стилями декораторов для выделения и усиления, аннотациями для ссылок и списками. Теперь мы хотим переопределить их с помощью звуковых концепций, имеющихся в SSML.
Мы начнем с определения различных структур содержимого с полезными описаниями для редакторов, которые мы добавим в block
в SSMLeditorSchema.js в качестве конфигураций для annotations
. Это «выделение», «псевдоним», «просодия» и «говорить как».
Акцент
Начнем с «акцента», который определяет, какой вес придается выделенному тексту. Мы определяем его как строку со списком предопределенных значений, из которых пользователь может выбирать:
// 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' } ] } } ] }
Псевдоним
Иногда письменный и устный термин различаются. Например, вы хотите использовать сокращение фразы в письменном тексте, но прочитать всю фразу вслух. Например:
<s>This is a <sub alias="Speech Synthesis Markup Language">SSML</sub> tutorial</s>
Поле ввода для псевдонима представляет собой простую строку:
// 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', } ] }
просодия
С помощью свойства просодии мы можем контролировать различные аспекты произнесения текста, такие как высота тона, скорость и громкость. Разметка для этого может выглядеть так:
<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>
Этот ввод будет иметь три поля с предопределенными строковыми параметрами:
// 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' } ] } } ] }
Скажи как
Последнее, что мы хотим включить, это <say-as>
. Этот тег позволяет нам немного больше контролировать, как произносится определенная информация. Мы даже можем использовать его, чтобы озвучивать слова, если вам нужно что-то отредактировать в голосовых интерфейсах. Это @!%& полезно!
<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' } ] } ] }
Теперь мы можем импортировать их в файл annotations.js , что немного упрощает работу.
// 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'
Теперь мы можем импортировать эти типы аннотаций в наши основные схемы:
// 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 ]) })
Наконец, теперь мы можем добавить их в редактор следующим образом:
// 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'} ] } } ] }
Обратите внимание, что мы также добавили пустые массивы в styles
и decorators
. Это отключает стили и декораторы по умолчанию (например, полужирный шрифт и выделение), поскольку в данном конкретном случае они не имеют особого смысла.
Настройка внешнего вида
Теперь у нас есть функциональность, но, поскольку мы не указали никаких значков, каждая аннотация будет использовать значок по умолчанию, что затрудняет фактическое использование редактора для авторов. Так давайте это исправим!
С помощью редактора Portable Text можно вводить компоненты React как для значков, так и для того, как должен отображаться выделенный текст. Здесь мы просто позволим некоторым смайликам сделать работу за нас, но вы, очевидно, могли бы пойти далеко с этим, сделав их динамичными и так далее. Для prosody
мы даже заставим иконку меняться в зависимости от выбранной громкости. Обратите внимание, что я пропустил поля в этих фрагментах для краткости, вам не следует удалять их в ваших локальных файлах.
// 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>, }, };
Теперь у вас есть редактор для редактирования текста, которым могут пользоваться голосовые помощники. Но разве не было бы полезно, если бы редакторы также могли предварительно просмотреть, как на самом деле будет звучать текст?
Добавление кнопки предварительного просмотра с помощью Google Text-to-Speech
Нативная поддержка синтеза речи для браузеров уже на подходе. Но в этом руководстве мы будем использовать Google Text-to-Speech API, который поддерживает SSML. Создание этой функции предварительного просмотра также будет демонстрацией того, как вы сериализуете Portable Text в SSML в любой службе, для которой вы хотите ее использовать.
Оборачиваем редактор в компонент React
Начнем с открытия файла SSMLeditor.js и добавления следующего кода:
// SSMLeditor.js import React, { Fragment } from 'react'; import { BlockEditor } from 'part:@sanity/form-builder'; export default function SSMLeditor(props) { return ( <Fragment> <BlockEditor {...props} /> </Fragment> ); }
Теперь мы обернули редактор в наш собственный компонент React. Все необходимые реквизиты, включая содержащиеся в нем данные, передаются в режиме реального времени. Чтобы на самом деле использовать этот компонент, вы должны импортировать его в свой файл 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' }, ], }, }, ], }
Когда вы сохраните это и студия перезагрузится, все должно выглядеть точно так же, но это потому, что мы еще не начали настраивать редактор.
Преобразование переносимого текста в SSML
Редактор сохранит содержимое как переносимый текст, массив объектов в формате JSON, который позволяет легко преобразовывать форматированный текст в любой формат, который вам нужен. Когда вы конвертируете Portable Text в другой синтаксис или формат, мы называем это «сериализацией». Следовательно, «сериализаторы» — это рецепты того, как следует преобразовывать форматированный текст. В этом разделе мы добавим сериализаторы для синтеза речи.
Вы уже создали файл blocksToSSML.js . Теперь нам нужно добавить нашу первую зависимость. Начните с запуска команды терминала npm init -y
внутри ssml-editor
. Это добавит package.json , в котором будут перечислены зависимости редактора.
Как только это будет сделано, вы можете запустить npm install @sanity/block-content-to-html
, чтобы получить библиотеку, упрощающую сериализацию Portable Text. Мы используем HTML-библиотеку, потому что SSML имеет тот же синтаксис XML с тегами и атрибутами.
Это куча кода, поэтому не стесняйтесь копировать и вставлять его. Я объясню шаблон прямо под фрагментом:
// 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 })
Этот код экспортирует функцию, которая берет массив блоков и перебирает их. Всякий раз, когда блок содержит mark
, он будет искать сериализатор для типа. Если вы пометили какой-то текст для emphasis
, это функция из объекта сериализатора:
emphasis: ({ children, mark: { level } }) => h('emphasis', { attrs: { level } }, children)
Может быть, вы узнаете параметр, из которого мы определили схему? Функция h()
позволяет нам определить элемент HTML, то есть здесь мы «обманываем» и заставляем возвращать элемент SSML с именем <emphasis>
. Мы также присвоим ему level
атрибута, если он определен, и поместим в него children
элементы — в большинстве случаев это будет текст, который вы выделили с помощью 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" ] } ] }
Вот как приведенная выше структура в Portable Text сериализуется в этот SSML:
<emphasis level="strong">Say this strongly</emphasis>
Если вам нужна поддержка большего количества тегов SSML, вы можете добавить дополнительные аннотации в схему и добавить типы аннотаций в раздел marks
в сериализаторах.
Теперь у нас есть функция, которая возвращает разметку SSML из нашего размеченного форматированного текста. Последняя часть — сделать кнопку, которая позволит нам отправить эту разметку в службу преобразования текста в речь.
Добавление кнопки предварительного просмотра, которая отвечает вам
В идеале мы должны были использовать возможности браузера по синтезу речи в веб-API. Таким образом, нам бы сошло с рук меньше кода и зависимостей.
Однако по состоянию на начало 2019 года встроенная браузерная поддержка синтеза речи все еще находится на ранней стадии. Похоже, что поддержка SSML уже на подходе, и есть доказательства концепций реализаций JavaScript на стороне клиента для него.
Скорее всего, вы все равно собираетесь использовать этот контент с голосовым помощником. И Google Assistant, и Amazon Echo (Alexa) поддерживают SSML в качестве ответов при выполнении. В этом руководстве мы будем использовать API преобразования текста в речь Google, который также хорошо звучит и поддерживает несколько языков.
Начните с получения ключа API, зарегистрировавшись в Google Cloud Platform (это будет бесплатно для первого обрабатываемого вами миллиона символов). После регистрации вы можете создать новый ключ API на этой странице.
Теперь вы можете открыть файл PreviewButton.js и добавить в него этот код:
// 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> }
Я свел код этой кнопки предварительного просмотра к минимуму, чтобы упростить следование этому руководству. Конечно, вы можете создать его, добавив состояние, чтобы показать, обрабатывается ли предварительный просмотр, или сделать возможным предварительный просмотр с различными голосами, которые поддерживает API Google.
Добавьте кнопку в 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> ); }
Теперь вы сможете пометить свой текст различными аннотациями и услышать результат при нажатии «Произнести текст». Круто, не так ли?
Вы создали редактор синтеза речи, и что теперь?
Если вы следовали этому руководству, вы узнали, как использовать редактор Portable Text в Sanity Studio для создания пользовательских аннотаций и настройки редактора. Вы можете использовать эти навыки для чего угодно, не только для создания редактора синтеза речи. Вы также узнали, как сериализовать Portable Text в нужный вам синтаксис. Очевидно, это также удобно, если вы создаете интерфейсы в React или Vue. Вы даже можете использовать эти навыки для создания Markdown из Portable Text.
Мы не рассказали, как вы на самом деле используете это вместе с голосовым помощником. Если вы хотите попробовать, вы можете использовать ту же логику, что и с кнопкой предварительного просмотра в бессерверной функции, и установить ее в качестве конечной точки API для выполнения с помощью веб-перехватчиков, например, с помощью Dialogflow.
Если вы хотите, чтобы я написал руководство о том, как использовать редактор синтеза речи с голосовым помощником, дайте мне подсказку в Твиттере или поделитесь в разделе комментариев ниже.
Дальнейшее чтение на SmashingMag:
- Эксперименты с синтезом речи
- Улучшение взаимодействия с пользователем с помощью Web Speech API
- API специальных возможностей: ключ к веб-доступности
- Создание простого чат-бота с искусственным интеллектом с помощью Web Speech API и Node.js