ナレーション
各ページを読み上げ、読み上げ中の文をハイライトする「このページを聞く」プレーヤーです。読者のブラウザの音声、またはビルド時に生成されるニューラル音声を使用します。
ナレーション機能を有効にすると、各ページの説明文の下に このページを聞く プレーヤーが表示されます。読者が再生ボタンを押すと、ページがタイトルから順に読み上げられます。読み上げ中の文はハイライトされ、常に画面内に表示されます。この機能はオプトインです:
export default defineConfig({
narration: true,
});
true を指定すると、読者自身のブラウザの音声でページが読み上げられます。キーもビルドステップも必要ありません。静的ホスティングかどうかにかかわらず、どのホストでも動作します。代わりに生成音声を使う場合は、provider を追加してください。
読み上げる内容
ナレーションは、ページのタイトル、説明文、見出し、段落、リスト項目、カードのテキストを順番に読み上げます。一部のコンポーネントでは、先に短い音声の合図が入ります。これにより、聞き手は次にどのような種類のコンテンツが来るのかを把握できます:
| コンポーネント | 合図 |
|---|---|
| コールアウト | 「注記。」「ヒント。」「警告。」など |
| ステップ | 「ステップ 1。」「ステップ 2。」 |
| タブ | 「macOS タブ。」開いているタブだけでなく、すべてのタブが読み上げられます |
| アコーディオンと Expandable | 「展開可能なセクション。」 |
閉じたセクションや非表示のタブに到達すると、ナレーションはそれを自動で開きます。そのため、ハイライトされるテキストは常に読者の目に見える状態になります。
コードブロック、表、画像、動画、図、数式、型テーブル、ファイルツリー、ライブコンポーネントプレビューはスキップされます。これらは読み上げても意味をなさないためです。
プレーヤーが表示されるのは、聞く価値のある量の文章(おおよそ 50 語)があるページだけです。非常に短いページや、大部分がコードや API フィールドで占められているページには表示されません。
再生
ページの再生中、プレーヤーはヘッダーの下に固定されます。プレーヤーには、再生・一時停止、前の文・次の文への移動、進行状況スライダー、ページ全体の長さに対する経過時間、0.8× から 2× までの速度調整があります。設定した速度は次のページにも引き継がれます。
ページは、読み上げ中の文が画面内に表示されるように自動でスクロールします。読者が別の場所へスクロールすると追従は止まりますが、音声の再生は続きます。読み上げ中の文までスクロールして戻るか、読み上げ箇所を追う を押すと、追従が再開されます。別のページを開くと、ナレーションは停止します。
ブラウザの音声
narration: true の場合、Web Speech API と読者のデバイスにある音声を使ってページを読み上げます。音声はページの言語に合わせて選ばれます。その言語の音声がないブラウザでは、誤ったアクセントで読み上げることを避けるため、プレーヤーは非表示のままになります。
この方法では、準備も費用も必要ありません。ただし、音声の品質はデバイスによって異なります。macOS、iOS、Windows のシステム音声は高品質ですが、一部の Linux ブラウザには音声がほとんど、またはまったく入っていません。
生成音声
provider を渡すと、ビルド時にニューラル音声モデルで音声を生成します。プロバイダーには、アシスタントのアダプターのいずれかを使い、音声モデルを指定します。Vercel AI Gateway の音声モデルを使う場合は gateway() を、OpenAI 独自の音声モデルや自分で運用している音声サーバーを使う場合は openai() を使用します。
import { defineConfig } from "blume";
import { gateway } from "blume/ai";
export default defineConfig({
narration: {
provider: gateway({
model: "openai/tts-1-hd",
voice: "alloy",
}),
},
});
blume build は、ビルドされた各ページをプレーヤーとまったく同じ方法で読み取り、文ごとに分割します。そして、プロバイダー(この例では Vercel AI Gateway)を通じて文ごとに 1 つのクリップを生成します。クリップとページごとの小さなマニフェストは、静的ファイルとして /blume-narration/ 以下に出力されます。そのため、繰り返し再生しても費用はかからず、サーバー上で処理が実行されることもありません。
ビルドは生成を始める前に、必要な新しいクリップの数とその合計文字数をログに出力します。そのため、コストを事前に確認できます:
Generating narration: 214 new clip(s), 15,880 characters, with openai/tts-1-hd
gateway() には次のオプションがあります:
| オプション | デフォルト | 説明 |
|---|---|---|
model |
openai/tts-1-hd |
ゲートウェイの音声モデルです。openai/tts-1、openai/tts-1-hd、fish-audio/s2.1-pro、spacexai/grok-tts のほか、ゲートウェイに掲載されている他のモデルも指定できます。 |
voice |
alloy |
モデルの音声です。 |
instructions |
指示に対応したモデルで、音声の話し方を指定します(例:「教師のように落ち着いて読んでください」)。 | |
apiKeyEnv |
AI_GATEWAY_API_KEY |
ゲートウェイのキーを格納する環境変数です。Vercel では、ビルドの OIDC トークンも使用できます。 |
headers |
すべてのリクエストに付加される静的ヘッダーです。 | |
providerOptions |
AI SDK の generateSpeech にそのまま渡されます。Blume にオプションが用意されていないモデル設定に使います。 |
OpenAI とセルフホストの音声
openai() は、代わりに OpenAI の音声 API でクリップを生成します。OPENAI_API_KEY を使って OpenAI 独自のモデルを利用できるほか、baseUrl を指定すれば、同じ POST /audio/speech エンドポイントを提供する任意のサーバーも利用できます。たとえば Kokoro-FastAPI、Speaches、音声モデルを備えた LiteLLM プロキシなどです。ビルドからホスト型サービスにアクセスできないサイトでも、この方法で生成音声を使えます:
import { defineConfig } from "blume";
import { openai } from "blume/ai";
export default defineConfig({
narration: {
provider: openai({
baseUrl: "http://localhost:8880/v1",
model: "kokoro",
voice: "af_heart",
}),
},
});
LiteLLM プロキシのようにキーを必要とするサーバーには、apiKeyEnv で指定したキーが送信されます(openai({ baseUrl, apiKeyEnv: "LITELLM_API_KEY", model }))。apiKeyEnv を指定しない場合、baseUrl のサーバーはキーなしで呼び出されます。そのため、OPENAI_API_KEY が別のサーバーに送られることはありません。MP3 以外のレスポンス(サーバーの前段にあるプロキシが返すサインインページや、要求された形式を無視するサーバーが返す WAV など)は、キャッシュされずに失敗したクリップとして扱われます。
baseUrl の有無にかかわらず、openai() は AI SDK の OpenAI プロバイダーを通じて音声を生成します。そのため、プロジェクトに @ai-sdk/openai を追加してください(npm install @ai-sdk/openai)。このパッケージがない場合、blume build は Vite の実行前に停止し、不足しているパッケージ名を表示します。
| オプション | デフォルト | 説明 |
|---|---|---|
model |
必須です。音声モデルを指定します。OpenAI では gpt-4o-mini-tts、tts-1、tts-1-hd を、それ以外ではサーバーが提供するモデルの ID を指定します。 |
|
baseUrl |
サーバーの OpenAI 互換のベース URL です(/v1 を含みます)。指定しない場合、クリップは OpenAI から取得されます。ただし OPENAI_BASE_URL が設定されている場合は、OpenAI 公式の SDK と同様に、そのサーバーから取得されます。 |
|
voice |
alloy |
モデルの音声です。 |
instructions |
指示に対応したモデル(gpt-4o-mini-tts)で、音声の話し方を指定します。 |
|
apiKeyEnv |
baseUrl がない場合は OPENAI_API_KEY |
キーを格納する環境変数です。キーはベアラートークンとして送信されます。baseUrl を指定した場合は、ここで指定したキーだけが送信されるため、OPENAI_API_KEY が別のサーバーに届くことはありません。何も指定しなければ、リクエストはベアラートークンなしで送信されます(headers で設定した Authorization は引き続き送信されます)。 |
headers |
すべてのリクエストに付加される静的ヘッダーです。 | |
providerOptions |
AI SDK の generateSpeech にそのまま渡されます。OpenAI の音声 API は、ここから speed を読み取ります:{ openai: { speed: 1.1 } }。 |
OpenAI の音声 API は言語の指定を受け付けないため、openai() はページの言語を送信しません。また、複数の言語に登場する同じ文は 1 回だけ生成されます。OpenAI のモデルはテキストから言語を判別しますが、多くのセルフホストのサーバーでは音声によって言語が決まります。そのため、コンテンツの言語を話せる音声を選んでください。
キャッシュ
クリップは node_modules/.cache/blume/narration にキャッシュされます。キャッシュのキーは、音声の内容を決める要素です。具体的には、文、その言語(言語を送信する gateway() の場合)、モデル、音声、指示、providerOptions、そして openai() の場合は呼び出し先のサーバーです。baseUrl を別のサーバーに向けると、モデルと音声が同じでも、すべてのクリップが再生成されます。再ビルド時に費用が発生するのは変更された文だけです。また、複数のページに共通する文は 1 回だけ生成されます。Vercel と Netlify はビルドキャッシュから node_modules を復元するため、デプロイ時には変更された部分だけが再生成されます。その他の CI では、実行間でこのディレクトリをキャッシュしてください。
フォールバック
生成音声は、クリップが存在しない場合にブラウザの音声にフォールバックします:
blume devの場合。blume devは音声を生成しません。生成音声をローカルで確認するには、blume buildとblume previewを実行してください。- ビルド時にキーが設定されていない場合。ビルドは警告を出し、新しいクリップを生成しません。ただし、すべてのクリップがキャッシュ済みのページでは、引き続きそのクリップが使われます。
baseUrlを指定しapiKeyEnvを指定していないopenai()はキーを送信しないため、そのまま生成を続けます。 - 生成に失敗した場合(ビルドからサーバーにアクセスできない場合を含みます)。AI SDK がすでにリトライしているため、生成は最初に失敗したクリップで停止します。ビルドは警告を出して続行し、キャッシュ済みのクリップはすべて保持されます。
言語
ナレーションは、各ページをそのコンテンツの言語で読み上げます。そのため、国際化されたサイトでは、ロケールごとにその言語の音声で読み上げられます。openai() の場合、これはモデルと音声によって異なります。詳しくは OpenAI とセルフホストの音声を参照してください。音声の合図とプレーヤーのラベルは、すべての組み込み UI 言語に翻訳されています。ロケールごとに上書きする場合は、i18n.ui の narration で指定してください。
ページごとに無効にする
ページのフロントマターで narration: false を設定すると、そのページにはプレーヤーが表示されなくなります:
---
title: Changelog
narration: false
---
コンテンツを除外する
要素に data-blume-narration="skip" を追加すると、その要素と内部のすべてのコンテンツがナレーションの対象外になります。Blume が自身の型テーブルやプレビューをスキップしているのもこの仕組みです。独自のコンポーネントやアイランドでも同じように機能します:
<div data-blume-narration="skip">
<PricingCalculator />
</div>
アナリティクス
読者が再生を始めると、プレーヤーは narration_play イベントを送信します。このイベントには engine(audio または browser)とページの path が含まれます。ページが最後まで再生されると、narration_complete イベントを送信します。どちらのイベントも、他のカスタムイベントと同じくアナリティクスアダプターを通じて送信されます。