エージェントに音声機能を追加
本文の状態
日本語全文を表示中
詳細モードで約17分の本文を読めます。
同じ出来事の情報源
6媒体で確認
Cloudflare Blog · The Decoder · Simon Willison Blog · TLDR AI · 404 Media · Ars Technica AI
各社の報じ方を比較 ↓OpenAIがAIエージェントに音声機能を追加し、テキスト入力に依存しない自然な会話を可能にした。これにより、通勤中やマルチタスク時など、より多様な状況での活用が期待される。
Source Article
元記事を日本語で読む
本文に関係しない購読案内、埋め込み通知、サイト内プロモーションは除いています。
私たち多くの人が AI エージェントと接する最初の経験は、チャットボックスに入力することでした。そして、日常的にエージェントを使用している私たちにとって、それらを誘導するために詳細なプロンプトやマークダウンファイルを書くことに長けているはずです。
しかし、エージェントが最も有用となる瞬間は、必ずしもテキスト中心ではない場合があります。長時間の通勤中である場合、複数のオープンセッションを同時に処理している場合、あるいは単にエージェントと自然に会話し、その返答を受け取り、対話を継続したい場合などです。
エージェントに音声機能を追加するために、そのエージェントを別の音声フレームワークに移行する必要はありません。本日、Agents SDK 用の実験的な音声パイプラインをリリースします。
@cloudflare/voice を使用すれば、すでに使用している同じエージェントアーキテクチャにリアルタイム音声を追加できます。音声は、単に同じ Durable Object に対して、Agents SDK が既に提供しているのと同じツール、永続性、WebSocket 接続モデルを用いて対話する別の方法となります。
@cloudflare/voice は、Agents SDK 用の実験的なパッケージであり、以下の機能を提供します:
完全な会話型音声エージェント向けの withVoice(Agent)
Dictation や音声検索など、テキストへの音声変換(Speech-to-Text)のみのユースケース向けの withVoiceInput(Agent)
React アプリケーション向けの useVoiceAgent および useVoiceInput フック
フレームワーク非依存のクライアント向けの VoiceClient
外部 API キーなしで開始できるようにする、組み込みの Workers AI プロバイダー:
Deepgram Flux による継続的な STT(Speech-to-Text)
Deepgram Nova 3 による継続的な STT(Speech-to-Text)
Deepgram Aura による Text-to-Speech
これは、単一の WebSocket 接続を通じてユーザーとリアルタイムに会話できるエージェントを構築可能であることを意味します。同時に、同じ Agent クラス、Durable Object インスタンス、そして SQLite をバックエンドとする会話履歴を維持したままです。
それと同様に重要なのは、これが単一の固定されたデフォルトスタックに限定されないことです。@cloudflare/voice 内のプロバイダーインターフェースは意図的に小さく設計されており、音声、テレフォニー、トランスポートのプロバイダーが私たちと共に開発することを望んでいます。これにより、開発者は単一の音声アーキテクチャに縛られることなく、ユースケースに適したコンポーネントを自由に組み合わせることができます。
音声機能の始め方
Agents SDK における音声エージェントの最小限のサーバーサイドパターンは以下の通りです:
import { Agent, routeAgentRequest } from "agents";
import {
withVoice,
WorkersAIFluxSTT,
WorkersAITTS,
type VoiceTurnContext
} from "@cloudflare/voice";
const VoiceAgent = withVoice(Agent);
export class MyAgent extends VoiceAgent {
transcriber = new WorkersAIFluxSTT(this.env.AI);
tts = new WorkersAITTS(this.env.AI);
async onTurn(transcript: string, context: VoiceTurnContext) {
return You said: ${transcript};
}
}
export default {
async fetch(request: Request, env: Env) {
return (
(await routeAgentRequest(request, env)) ??
new Response("Not found", { status: 404 })
);
}
} satisfies ExportedHandler;
これがサーバー全体の構成です。ここに連続トランスクリプター(continuous transcriber)とテキスト読み上げプロバイダー(text-to-speech provider)を追加し、onTurn() を実装します。
クライアント側では、React フックを使用して接続できます:
import { useVoiceAgent } from "@cloudflare/voice/react";
function App() {
const {
status,
transcript,
interimTranscript,
startCall,
endCall,
toggleMute
} = useVoiceAgent({ agent: "my-agent" });
return (
Status: {status}
{interimTranscript && *{interimTranscript}*
}
{transcript.map((msg, i) => (
- {msg.role}: {msg.text}
))}
Start Call
End Call
Mute / Unmute
);
}
React を使用していない場合は、@cloudflare/voice/client から直接 VoiceClient を使用できます。
How the voice pipeline works
Agents SDK では、すべてのエージェントは Durable Object(耐久性のあるオブジェクト)です。これは、独自の SQLite データベース、WebSocket 接続、およびアプリケーションロジックを持つ、状態を保持しアドレス可能なサーバーインスタンスです。音声パイプラインはこのモデルを置き換えるのではなく、拡張するものです。
高レベルでは、フローは以下のようになります:
image
パイプラインの各ステップは以下の通り分解されます:
オーディオ転送:ブラウザはマイクからの音声をキャプチャし、エージェントが既に使用している同じ WebSocket 接続を通じて、16 kHz モノラル PCM でストリーミングします。
STT セッションのセットアップ:通話開始時、エージェントは通話全体を通じて存続する継続的なトランスクリバー・セッションを作成します。
STT 入力:音声ストリームは、そのセッションに継続的に流入します。
STT 話者検出(Turn Detection):音声からテキストへの変換モデル自体が、ユーザーの発話が完了したと判断し、その話者(ターン)に対する安定した文字起こし結果を出力します。
LLM/アプリケーションロジック:音声パイプラインは、その文字起こし結果をあなたの onTurn() メソッドに渡します。
TTS 出力:応答は音声に合成され、クライアントへ送信されます。onTurn() がストリームを返す場合、パイプラインはそれを文単位に分割し、文が準備されるたびに音声の送信を開始します。
永続化:ユーザーとエージェントのメッセージは SQLite に保存されるため、再接続やデプロイ後も会話履歴が保持されます。
なぜ音声はエージェントの他の機能ととも成長すべきか
多くの音声フレームワークは、音声ループそのものに焦点を当てています:入力された音声を文字起こしし、モデルの応答を得て、出力する音声へ変換するという一連の流れです。これらは重要な基本要素ですが、エージェントには音声以外にも多くの側面があります。
本番環境で動作する実際のエージェントは成長していきます。それらは状態管理、スケジューリング、永続化、ツール、ワークフロー、電話回線(テレフォニー)の機能、そしてそれらすべての整合性をチャネル間で維持する方法を必要とします。エージェントの複雑さが増すにつれて、音声は単独の機能ではなく、より大きなシステムの一部分となります。
私たちは、Agents SDK における音声機能もその前提から始める必要がありました。音声を別のスタックとして構築するのではなく、同じ Durable Object ベースのエージェントプラットフォームの上に構築したため、後でアプリケーションを再設計することなく、必要な他のプリミティブを引き継ぐことができます。
音声とテキストは同じ状態を共有します
ユーザーはテキスト入力から始め、音声に切り替え、再びテキストに戻る可能性があります。Agents SDK では、これらはすべて同じエージェントに対する異なる入力に過ぎません。会話履歴は SQLite に保存され、利用可能なツールも同じです。これにより、より明確なメンタルモデルと、推論が容易な非常にシンプルなアプリケーションアーキテクチャが得られます。
低レイテンシは…
短いネットワークパスから生まれます
音声体験は、心地よいものか不快なものかがすぐに判断されます。ユーザーが話しを終えた後、システムは文字起こしを行い、思考し、会話らしく聞こえる速さで応答を話し始める必要があります。
音声レイテンシの多くは、モデル処理時間そのものではありません。それは、異なる場所にある異なるサービス間でオーディオとテキストを行き来するコストです。オーディオは STT(Speech-to-Text)に送信され、文字起こし結果は LLM に渡され、応答は TTS(Text-to-Speech)モデルに送られます。各ハンドオフにはネットワークオーバーヘッドが加算されます。
エージェント SDK の音声パイプラインを使用すると、エージェントは Cloudflare のネットワーク上で実行され、組み込みプロバイダーは Workers AI バインディングを利用します。これによりパイプラインが簡素化され、自分で構築する必要のあるインフラストラクチャの量を削減できます。
組み込みストリーミング
エージェントが最初の文を素早く話す(Time-to-First Audio、初回音声到達時間とも呼ばれる)と、対話体験はより自然に感じられます。onTurn() がストリームを返す場合、パイプラインはそれを文単位に分割し、文が完成するたびに音声合成を開始します。つまり、残りの部分がまだ生成されている間に、ユーザーは回答の冒頭を聞くことができます。
より現実的なバックエンド
以下は、LLM の応答をストリーミングし、文単位でそれを音声として返すより完全な例です:
import { Agent, routeAgentRequest } from "agents";
import {
withVoice,
WorkersAIFluxSTT,
WorkersAITTS,
type VoiceTurnContext
} from "@cloudflare/voice";
import { streamText } from "ai";
import { createWorkersAI } from "workers-ai-provider";
const VoiceAgent = withVoice(Agent);
export class MyAgent extends VoiceAgent {
transcriber = new WorkersAIFluxSTT(this.env.AI);
tts = new WorkersAITTS(this.env.AI);
async onTurn(transcript: string, context: VoiceTurnContext) {
const ai = createWorkersAI({ binding: this.env.AI });
const result = streamText({
model: ai("@cf/cloudflare/gpt-oss-20b"),
system: "You are a helpful voice assistant. Be concise.",
messages: [
...context.messages.map((m) => ({
role: m.role as "user" | "assistant",
content: m.content
})),
{ role: "user" as const, content: transcript }
],
abortSignal: context.signal
});
return result.textStream;
}
}
export default {
async fetch(request: Request, env: Env) {
return (
(await routeAgentRequest(request, env)) ??
new Response("Not found", { status: 404 })
);
}
} satisfies ExportedHandler;
Context.messages gives you recent SQLite-backed conversation history, and context.signal lets the pipeline abort the LLM call if the user interrupts.
Voice as an input: withVoiceInput
Not every speech interface needs to speak back. Sometimes you might want dictation, transcription, or voice search. For these use cases, you can use withVoiceInput
import { Agent, type Connection } from "agents";
import { withVoiceInput, WorkersAINova3STT } from "@cloudflare/voice";
const InputAgent = withVoiceInput(Agent);
export class DictationAgent extends InputAgent {
transcriber = new WorkersAINova3STT(this.env.AI);
onTranscript(text: string, _connection: Connection) {
console.log("User said:", text);
}
}
クライアント側では、useVoiceInput を使用して、文字起こしを中核とした軽量インターフェースを取得できます:
import { useVoiceInput } from "@cloudflare/voice/react";
const { transcript, interimTranscript, isListening, start, stop, clear } =
useVoiceInput({ agent: "DictationAgent" });
これは、音声が入力方法として機能し、完全な会話ループを必要としない場合に有用です。
同一接続上での音声とテキスト
同じクライアントは sendText("What’s the weather?") を呼び出すことができ、これは STT(音声認識)をバイパスしてテキストを直接 onTurn() に送信します。アクティブな通話中、応答は音声として再生され、同時にテキストとしても表示されます。通話外では、テキストのみの状態を維持することも可能です。
これにより、実装を異なるコードパスに分割することなく、真の意味でマルチモーダルなエージェントを実現できます。
その他に何を作れるか?
音声エージェントも依然としてエージェントであるため、通常の Agents SDK の機能はすべて適用されます。
ツールとスケジューリング
セッション開始時に発信者を歓迎する処理を行うことができます:
import { Agent, type Connection } from "agents";
import { withVoice, WorkersAIFluxSTT, WorkersAITTS } from "@cloudflare/voice";
const VoiceAgent = withVoice(Agent);
export class MyAgent extends VoiceAgent {
transcriber = new WorkersAIFluxSTT(this.env.AI);
tts = new WorkersAITTS(this.env.AI);
async onTurn(transcript: string) {
return You said: ${transcript};
}
async onCallStart(connection: Connection) {
await this.speak(connection, "Hi! How can I help you today?");
}
}
あなたは、他のエージェントと同様に、音声によるリマインダーをスケジュールしたり、LLM(大規模言語モデル)に対してツールを公開したりできます:
import { Agent } from "agents";
import {
withVoice,
WorkersAIFluxSTT,
WorkersAITTS,
type VoiceTurnContext
} from "@cloudflare/voice";
import { streamText, tool } from "ai";
import { createWorkersAI } from "workers-ai-provider";
import { z } from "zod";
const VoiceAgent = withVoice(Agent);
export class MyAgent extends VoiceAgent {
transcriber = new WorkersAIFluxSTT(this.env.AI);
tts = new WorkersAITTS(this.env.AI);
async speakReminder(payload: { message: string }) {
await this.speakAll(Reminder: ${payload.message});
}
async onTurn(transcript: string, context: VoiceTurnContext) {
const ai = createWorkersAI({ binding: this.env.AI });
const result = streamText({
model: ai("@cf/cloudflare/gpt-oss-20b"),
messages: [
...context.messages.map((m) => ({
role: m.role as "user" | "assistant",
content: m.content
})),
{ role: "user" as const, content: transcript }
],
tools: {
set_reminder: tool({
description: "Set a spoken reminder after a delay",
inputSchema: z.object({
message: z.string(),
delay_seconds: z.number()
}),
execute: async ({ message, delay_seconds }) => {
await this.schedule(delay_seconds, "speakReminder", { message });
return { confirmed: true };
}
})
},
abortSignal: context.signal
});
return result.textStream;
}
}
Runtime model switching
The voice pipeline also lets you choose a transcription model dynamically per connection.
For example, you might prefer Flux for conversational turn-taking and Nova 3 for higher-accuracy dictation. You can switch at runtime by overriding createTranscriber():
import { Agent, type Connection } from "agents";
import {
withVoice,
WorkersAIFluxSTT,
WorkersAINova3STT,
WorkersAITTS,
type Transcriber
} from "@cloudflare/voice";
export class MyAgent extends VoiceAgent {
tts = new WorkersAITTS(this.env.AI);
createTranscriber(connection: Connection): Transcriber {
const url = new URL(connection.url ?? "http://localhost");
const model = url.searchParams.get("model");
if (model === "nova-3") {
return new WorkersAINova3STT(this.env.AI);
}
return new WorkersAIFluxSTT(this.env.AI);
}
}
On the client, you can pass query parameters through the hook:
const voiceAgent = useVoiceAgent({
agent: "my-voice-agent",
query: { model: "nova-3" }
});
Pipeline hooks
You can also intercept data between stages:
afterTranscribe(transcript, connection)
beforeSynthesize(text, connection)
afterSynthesize(audio, text, connection)
These hooks are useful for content filtering, text normalization, language-specific transformations, or custom logging.
Telephone and transport options
By default, the voice pipeline uses a single WebSocket connection as the simplest path for 1:1 voice agents. But that’s not the only option.
Phone calls via Twilio
You can connect phone calls to the same agent using the Twilio adapter:
import { TwilioAdapter } from "@cloudflare/voice-twilio";
export default {
async fetch(request: Request, env: Env) {
if (new URL(request.url).pathname === "/twilio") {
return TwilioAdapter.handleRequest(request, env, "MyAgent");
}
return (
(await routeAgentRequest(request, env)) ??
new Response("Not found", { status: 404 })
);
}
};
これにより、同じエージェントがウェブ音声、テキスト入力、および電話通話を処理できるようになります。
1 つの注意点として、デフォルトの Workers AI TTS(Text-to-Speech:テキスト読み上げ)プロバイダーは MP3 を返しますが、Twilio は mulaw 8kHz のオーディオを期待しています。本番環境の電話回線では、PCM または mulaw を直接出力する TTS プロバイダーを使用することをお勧めします。
WebRTC
過酷なネットワーク条件により適している、または複数の参加者を含む必要があるトランスポートが必要な場合、音声パッケージには SFU(Selective Forwarding Unit:選択中継ユニット)ユーティリティも含まれており、カスタムトランスポートをサポートしています。デフォルトのモデルは今日では WebSocket ネイティブですが、グローバルな SFU インフラストラクチャに接続するためのより多くのアダプターを開発する予定です。
一緒に構築しよう
音声パイプラインは設計上、プロバイダーに依存しません。
内部では、各ステージは小さなインターフェースによって定義されます。トランスクリバー(文字起こしエンジン)は継続的なセッションを開き、到着するオーディオフレームを受け取ります。一方、TTS プロバイダーはテキストを取り込み、オーディオを返します。プロバイダーがオーディオ出力をストリーミングできる場合、パイプラインはそれを使用することもできます。
interface Transcriber {
createSession(options?: TranscriberSessionOptions): TranscriberSession;
}
interface TranscriberSession {
feed(chunk: ArrayBuffer): void;
close(): void;
}
interface TTSProvider {
synthesize(text: string, signal?: AbortSignal): Promise;
}
Agents SDK で音声サポートを、1 つの固定されたモデルとトランスポートの組み合わせだけで動作させるつもりはありませんでした。エコシステムが拡大する中で、デフォルトのパスをシンプルに保ちつつ、他のプロバイダーを簡単に組み込めるようにすることが私たちの目標でした。
ビルトインのプロバイダーは Workers AI を使用しているため、外部の API キーなしで開始できます:
会話型のストリーミング STT 用 WorkersAIFluxSTT
Dictation スタイルのストリーミング STT 用 WorkersAINova3STT
テキスト読み上げ(Text-to-Speech)用 WorkersAITTS
しかし、より大きな目標は相互運用性です。音声やボイスサービスを維持している場合、これらのインターフェースは小さ enough で、SDK の内部構造のすべてを理解する必要なく実装できます。ストリーミング音声を受け入れ、発話の境界を検出できる STT プロバイダーであれば、トランスクリバーインターフェースを満たすことができます。ストリーミング音声出力ができる TTS プロバイダーであれば、さらに理想的です。
以下の相互運用性について協力したいと考えています:
AssemblyAI、Rev.ai、Speechmatics などの STT プロバイダー、またはリアルタイム文字起こし API を提供するあらゆるサービス
PlayHT、LMNT、Cartesia、Coqui、Amazon Polly、Google Cloud TTS などの TTS プロバイダー
Vonage、Telnyx、Bandwidth などのプラットフォーム向けのテレフォニーアダプター
WebRTC データチャネル、SFU ブリッジ、その他の音声トランスポート層向けのトランスポート実装
また、個々のプロバイダーを超えたコラボレーションにも興味があります:
STT + LLM + TTS の組み合わせにおけるレイテンシベンチマーク
英語以外の音声エージェントに対する多言語サポートと、より良いドキュメント化
アクセシビリティの取り組み、特にマルチモーダルインターフェースや音声障害に関する部分について
音声インフラストラクチャを構築しており、第一級統合を確認したい場合は、PR(プルリクエスト)を開くか、お問い合わせください。
今すぐ試す
音声パイプラインは、現在実験的なパッケージとして利用可能です:
npm create cloudflare@latest -- --template cloudflare/agents-starter
@cloudflare/voice を追加し、エージェントにトランスクリバー(文字起こし機能)と TTS(テキスト読み上げ)プロバイダーを与え、デプロイして会話を開始してください。API リファレンスもご参照いただけます。
興味深いものを作成した場合は、github.com/cloudflare/agents 上で issue または PR を開いてください。音声には別個のスタックを必要とすべきではなく、私たちは、他のすべてのものと同じ耐久性のあるアプリケーションモデル(durable application model)上で構築された最高の音声エージェントが生まれると考えています。
image
同じ出来事を6媒体で確認
同じ出来事を扱う別媒体の記事です。見出しと公開時刻を比較できます。
今日のまとめ
AIデイリーブリーフで今日の重要ニュースをまとめ読み