音声合成エディタの作り方
公開: 2022-03-10スティーブ・ジョブズが1984年にマッキントッシュを発表したとき、それはステージから私たちに「こんにちは」と言った。 その時点でさえ、音声合成は実際には新しいテクノロジーではありませんでした。ベルラボは早くも30年代後半にボコーダーを開発し、スタンリーキューブリックがボコーダーを2001年のHAL9000:宇宙の旅(1968)。
音声インターフェースが実際に広く一般の人々の家、手首、ポケットに浸透したのは、2015年代半ばにAppleのSiri、Amazon Echo、Googleアシスタントが導入される前ではありませんでした。 私たちはまだ採用段階にありますが、これらの音声アシスタントはまだ残っているようです。
言い換えれば、 Webはもはや画面上の単なる受動的なテキストではありません。 WebエディターとUXデザイナーは、大声で話す必要のあるコンテンツとサービスの作成に慣れなければなりません。
私たちはすでに、コンテンツをヘッドレスでAPIを介して操作できるコンテンツ管理システムの使用に向けて急速に進んでいます。 最後の部分は、音声用にコンテンツを簡単に調整できるようにする編集インターフェイスを作成することです。 だからそれをやろう!
SSMLとは
WebブラウザはW3CのHyperTextMarkup Language(HTML)仕様を使用してドキュメントを視覚的にレンダリングしますが、ほとんどの音声アシスタントは音声を生成するときにSpeech Synthesis Markup Language(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>
(ピッチ)のタグを導入するときです。
<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にはさらに多くの機能がありますが、基本を理解するにはこれで十分です。 それでは、音声合成編集インターフェースを作成するために使用するエディターを詳しく見てみましょう。
ポータブルテキスト用エディタ
このエディターを作成するには、Sanity.ioの機能を備えたポータブルテキスト用のエディターを使用します。 Portable Textは、リッチテキスト編集用のJSON仕様であり、SSMLなどの任意のマークアップ言語にシリアル化できます。 これは、異なるマークアップ言語を使用して、同じテキストスニペットを複数の場所で簡単に使用できることを意味します。
Sanityのインストール
Sanity.ioは、React.jsで構築されたオープンソースの編集環境を備えた構造化コンテンツのプラットフォームです。 すべてを稼働させるには2分かかります。
ターミナルにnpm i -g @sanity/cli && sanity init
と入力し、指示に従います。 プロジェクトテンプレートの入力を求められたら、「空」を選択します。
このチュートリアルに従ってこのエディターを最初から作成したくない場合は、このチュートリアルのコードのクローンを作成して、 README.md
の指示に従うこともできます。
エディターがダウンロードされたら、プロジェクトフォルダーでsanity start
を実行してエディターを起動します。 ホットモジュールの再読み込みを使用して、ファイルを編集するときに変更を更新する開発サーバーを起動します。
SanityStudioでスキーマを構成する方法
エディタファイルの作成
まず、 / schemasフォルダーにssml-editorというフォルダーを作成します。 そのフォルダに、いくつかの空のファイルを配置します。
/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
これで、これらのファイルにコンテンツスキーマを追加できます。 コンテンツスキーマは、リッチテキストのデータ構造を定義するものであり、SanityStudioが編集インターフェイスを生成するために使用するものです。 これらは単純なJavaScriptオブジェクトであり、ほとんどの場合、 name
とtype
のみが必要です。
title
とdescription
を追加して、編集者にとって少し便利にすることもできます。 たとえば、これはtitle
の単純なテキストフィールドのスキーマです。
export default { name: 'title', type: 'string', title: 'Title', description: 'Titles should be short and descriptive' }
ポータブルテキストは、データとしてのリッチテキストの概念に基づいて構築されています。 これは、リッチテキストをクエリし、必要なほとんどすべてのマークアップに変換できるため、強力です。
これは、「段落」と考えることができる「ブロック」と呼ばれるオブジェクトの配列です。 ブロック内には、子スパンの配列があります。 各ブロックには、子スパンに分散されたデータ構造を記述するスタイルと一連のマーク定義を含めることができます。
Sanity.ioには、Portable Textの読み取りと書き込みが可能なエディターが付属しており、次のように、 block
タイプをarray
フィールド内に配置することでアクティブになります。
// speech.js export default { name: 'speech', type: 'array', title: 'SSML Editor', of: [ { type: 'block' } ] }
配列は複数のタイプにすることができます。 SSMLエディターの場合、これらはオーディオファイルのブロックである可能性がありますが、このチュートリアルの範囲外です。
最後に、このエディターを使用できるコンテンツタイプを追加します。 ほとんどのアシスタントは、「意図」と「履行」の単純なコンテンツモデルを使用します。
- インテント
通常、AIモデルがユーザーがやりたいことを描写するために使用する文字列のリスト。 - フルフィルメント
これは、「意図」が特定されたときに発生します。 フルフィルメントは、多くの場合、または少なくとも、何らかの応答を伴います。
それでは、音声合成エディタを使用するfulfillment
と呼ばれる単純なコンテンツタイプを作成しましょう。 fulfillment.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にある聴覚概念でそれらをオーバーライドしたいと思います。
まず、さまざまなコンテンツ構造を定義し、エディターに役立つ説明を付けて、 annotations
の構成としてSSMLeditorSchema.jsのblock
に追加します。 それらは、「強調」、「エイリアス」、「韻律」、および「言う」です。
強調
まず、マークされたテキストにどの程度の重みをかけるかを制御する「強調」から始めます。 これは、ユーザーが選択できる事前定義された値のリストを含む文字列として定義されます。
// 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>
この入力には、事前定義された文字列オプションを持つ3つのフィールドがあります。
// 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の音声合成を使用したプレビューボタンの追加
ネイティブ音声合成のサポートは、実際にはブラウザで進行中です。 ただし、このチュートリアルでは、SSMLをサポートするGoogleのText-to-SpeechAPIを使用します。 このプレビュー機能の構築は、これを使用するサービスでポータブルテキストを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のオブジェクトの配列です。 ポータブルテキストを別の構文または形式に変換する場合、それを「シリアル化」と呼びます。 したがって、「シリアライザー」は、リッチテキストを変換する方法のレシピです。 このセクションでは、音声合成用のシリアライザーを追加します。
あなたはすでにblocksToSSML.jsファイルを作成しました。 次に、最初の依存関係を追加する必要があります。 ssml-editor
フォルダー内でターミナルコマンドnpminit- npm init -y
を実行することから始めます。 これにより、エディターの依存関係が一覧表示されるpackage.jsonが追加されます。
それが完了したら、 npm install @sanity/block-content-to-html
を実行して、ポータブルテキストのシリアル化を容易にするライブラリを取得できます。 SSMLにはタグと属性を持つ同じXML構文があるため、HTMLライブラリを使用しています。
これは大量のコードなので、自由にコピーして貼り付けてください。 スニペットのすぐ下のパターンについて説明します。
// 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要素を定義できます。つまり、ここでは「チート」して、 <emphasis>
というSSML要素を返すようにします。 また、定義されている場合は属性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" ] } ] }
これが、PortableTextの上記の構造がこのSSMLにシリアル化される方法です。
<emphasis level="strong">Say this strongly</emphasis>
より多くのSSMLタグのサポートが必要な場合は、スキーマにさらに注釈を追加し、シリアライザーのmarks
セクションに注釈タイプを追加できます。
これで、マークアップされたリッチテキストからSSMLマークアップを返す関数ができました。 最後の部分は、このマークアップをテキスト読み上げサービスに送信できるボタンを作成することです。
あなたに話しかけるプレビューボタンを追加する
理想的には、WebAPIでブラウザの音声合成機能を使用する必要がありました。 そうすれば、コードと依存関係が少なくて済みます。
ただし、2019年の初めの時点で、音声合成に対するネイティブブラウザのサポートはまだ初期段階にあります。 SSMLのサポートが進行中であるように見え、クライアント側のJavaScript実装の概念実証があります。
とにかく音声アシスタントでこのコンテンツを使用する可能性があります。 GoogleアシスタントとAmazonEcho(Alexa)はどちらも、フルフィルメントの応答としてSSMLをサポートしています。 このチュートリアルでは、Googleのテキスト読み上げAPIを使用します。このAPIも優れたサウンドであり、いくつかの言語をサポートしています。
Google Cloud PlatformにサインアップしてAPIキーを取得することから始めます(処理する最初の100万文字は無料です)。 サインアップしたら、このページで新しい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> }
このチュートリアルを簡単に実行できるように、このプレビューボタンのコードは最小限に抑えています。 もちろん、プレビューが処理中であるかどうかを示すために状態を追加することによってそれを構築することも、GoogleのAPIがサポートするさまざまな音声でプレビューすることを可能にすることもできます。
ボタンを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> ); }
これで、テキストをさまざまな注釈でマークアップし、「テキストを話す」を押したときに結果を聞くことができるはずです。 かっこいいですね。
音声合成エディタを作成しましたが、今は何ですか?
このチュートリアルを実行した場合は、SanityStudioでポータブルテキスト用のエディターを使用してカスタム注釈を作成し、エディターをカスタマイズする方法を理解しました。 これらのスキルは、音声合成エディタを作成するだけでなく、あらゆる種類の用途に使用できます。 また、ポータブルテキストを必要な構文にシリアル化する方法についても説明しました。 もちろん、これはReactまたはVueでフロントエンドを構築している場合にも便利です。 これらのスキルを使用して、ポータブルテキストからマークダウンを生成することもできます。
音声アシスタントと一緒に実際にこれをどのように使用するかについては説明していません。 試してみたい場合は、サーバーレス関数のプレビューボタンとほぼ同じロジックを使用し、DialogflowなどのWebhookを使用してフルフィルメントのAPIエンドポイントとして設定できます。
音声合成エディタを音声アシスタントで使用する方法についてのチュートリアルを作成したい場合は、Twitterでヒントを提供するか、以下のコメントセクションで共有してください。
SmashingMagの詳細:
- SpeechSynthesisの実験
- Web SpeechAPIによるユーザーエクスペリエンスの向上
- アクセシビリティAPI:Webアクセシビリティへの鍵
- Web SpeechAPIとNode.jsを使用したシンプルなAIチャットボットの構築