はじめに
実務で会議の音声をリアルタイムで文字起こしする機能を実装する際に、Deepgramという音声認識サービスを使用しました。
実装を始めた当初、QiitaやZennでDeepgramのリアルタイム文字起こしに関する記事が見つからず、公式ドキュメントのサンプルコードも最小限の実装例のみで、個人的にかなり分かりづらいと感じました。
そのため、本記事では、Deepgramのリアルタイム音声文字起こし機能について、実装時に理解に時間がかかったポイントを含めて解説します。
この記事で話すこと・話さないこと
話すこと
- Deepgramのリアルタイム音声文字起こしの使い方
- よく使うオプションと実装のポイント
話さないこと
- 音声の取得処理(マイク入力など)
- 料金や課金の詳細
Deepgramとは?
Deepgramは、音声AI APIプラットフォームです。独自のディープラーニングモデルを使用し、音声認識から音声合成、音声分析まで、包括的な音声処理ソリューションを提供しています。
Deepgramの主なサービス
Deepgramは以下のようなサービスを提供しています。
-
Speech-to-Text(音声認識)
- 録音済み音声ファイルの文字起こし(Pre-recorded)
- リアルタイム音声認識(Live Streaming) ← 本記事で扱う
-
Text-to-Speech(音声合成)
- テキストから自然な音声を生成
-
Voice Agent API
- 対話型音声エージェントの構築
-
Audio Intelligence(音声解析)
- 感情分析、トピック検出、要約、エンティティ検出など
リアルタイム文字起こし(Live Transcription)の機能
本記事で紹介するLive Transcriptionは、WebSocketを使ってリアルタイムで音声を文字起こしする機能です。以下のような特徴があります。
- リアルタイム処理: 音声データを送信すると即座に文字起こし結果を受信
- 高精度な認識: nova-2、nova-3などの最新AIモデルを使用
- 話者分離(Diarization): 複数人の会議で誰がどの部分を話したかを自動識別
- 多言語対応: 日本語を含む多数の言語に対応
- 中間結果の取得: 発話途中の文字起こし結果をリアルタイムに表示可能
- 豊富なカスタマイズ: 句読点の自動挿入、スマートフォーマット、個人情報の自動削除など
Deepgramのインストール
npm install @deepgram/sdk
pnpm install @deepgram/sdk
yarn add @deepgram/sdk
環境変数にAPIキーを設定します。
DEEPGRAM_API_KEY=your_api_key_here
APIキーはDeepgramのコンソールから取得できます。
Deepgramの使い方
ここでは、1人のユーザーが音声データを送信し、リアルタイムで文字起こし結果を受け取る簡易的な実装例を紹介します。
処理の流れ
まず、全体の処理フローを図で示します。
全体コード
まず、全体の流れを把握するために完全なコードを示します。
import { createClient, LiveTranscriptionEvents, SOCKET_STATES } from '@deepgram/sdk';
import { on } from 'events';
// 1. クライアントの作成
const deepgramClient = createClient(process.env.DEEPGRAM_API_KEY);
// 2. 接続オプションの設定
const options = {
model: 'nova-2',
language: 'ja',
encoding: 'linear16',
sample_rate: 16000,
interim_results: true,
smart_format: true,
punctuate: true,
};
// 3. WebSocket接続を確立
const connection = deepgramClient.listen.live(options);
let keepAliveTimer: NodeJS.Timeout;
// 4. 接続完了イベント
connection.on(LiveTranscriptionEvents.Open, () => {
console.log('接続が確立されました');
// KeepAliveを開始
keepAliveTimer = setInterval(() => {
if (connection.getReadyState() === SOCKET_STATES.open) {
connection.keepAlive();
}
}, 5000);
// エラーイベント
connection.on(LiveTranscriptionEvents.Error, (error) => {
console.error('エラー:', error);
});
// 接続終了イベント
connection.on(LiveTranscriptionEvents.Close, () => {
console.log('接続が閉じられました');
if (keepAliveTimer) {
clearInterval(keepAliveTimer);
}
});
});
// 5. 音声データの送信
function sendAudioData(audioData: ArrayBuffer) {
if (connection.getReadyState() !== SOCKET_STATES.open) {
console.warn('接続が開いていません');
return;
}
const uint8Array = new Uint8Array(audioData);
connection.send(uint8Array.buffer);
}
// 6. 文字起こし結果の受信(AsyncIterableIterator)
async function* streamTranscript() {
const stream = on(connection, LiveTranscriptionEvents.Transcript);
for await (const [event] of stream) {
const transcript = event.channel.alternatives[0]?.transcript;
if (!transcript) continue;
yield {
transcript,
isFinal: event.is_final,
speechFinal: event.speech_final,
startTime: event.start,
};
}
}
// 7. 使用例
for await (const result of streamTranscript()) {
console.log(result.transcript);
if (result.isFinal) {
await saveToDatabase(result);
}
}
// 8. 接続の切断
function disconnect() {
if (keepAliveTimer) {
clearInterval(keepAliveTimer);
}
connection.requestClose();
}
以下、各ステップを詳しく説明します。
1. クライアントの作成
import { createClient } from '@deepgram/sdk';
const deepgramClient = createClient(process.env.DEEPGRAM_API_KEY);
2. 接続の確立
import { LiveTranscriptionEvents } from '@deepgram/sdk';
// 接続オプションの設定
const options = {
model: 'nova-2', // 使用するモデル
language: 'ja', // 言語(日本語)
encoding: 'linear16', // 音声エンコーディング形式
sample_rate: 48000, // サンプルレート
interim_results: true, // 中間結果を受信
smart_format: true, // スマートフォーマット有効化
punctuate: true, // 句読点の自動挿入
};
// WebSocket接続を確立
const connection = deepgramClient.listen.live(options);
// 接続完了イベント
connection.on(LiveTranscriptionEvents.Open, () => {
console.log('接続が確立されました');
// エラーイベント
connection.on(LiveTranscriptionEvents.Error, (error) => {
console.error('エラー:', error);
});
// 接続終了イベント
connection.on(LiveTranscriptionEvents.Close, () => {
console.log('接続が閉じられました');
});
});
ErrorとCloseのリスナーは、Openイベントの中で登録しています。これは、接続が確立されてから初めてこれらのイベントが発生する可能性があるためです。Openの外で登録すると、接続確立前に発生したイベントを取りこぼす可能性があります。
3. 音声データの送信
import { SOCKET_STATES } from '@deepgram/sdk';
function sendAudioData(audioData: ArrayBuffer) {
// 接続が開いているか確認(重要!)
if (connection.getReadyState() !== SOCKET_STATES.open) {
console.warn('接続が開いていません');
return;
}
// 音声データを送信
const uint8Array = new Uint8Array(audioData);
connection.send(uint8Array.buffer);
}
4. 文字起こし結果の受信
方法1: イベントリスナーを使う
connection.on(LiveTranscriptionEvents.Transcript, (data) => {
const transcript = data.channel.alternatives[0]?.transcript;
if (!transcript) return;
console.log('文字起こし結果:', transcript);
console.log('確定結果:', data.is_final);
console.log('発話終了:', data.speech_final);
});
方法2: AsyncIterableIteratorを使う(推奨)
import { on } from 'events';
async function* streamTranscript() {
const stream = on(connection, LiveTranscriptionEvents.Transcript);
for await (const [event] of stream) {
const transcript = event.channel.alternatives[0]?.transcript;
if (!transcript) continue;
yield {
transcript,
isFinal: event.is_final,
speechFinal: event.speech_final,
startTime: event.start,
};
}
}
// 使用例
for await (const result of streamTranscript()) {
console.log(result.transcript);
if (result.isFinal) {
// 確定結果の処理
await saveToDatabase(result);
}
}
方法2が推奨される理由:
- 処理順序の保証: イベントが時系列順に処理される(会議の文字起こしで重要)
- バックプレッシャー制御: 処理完了まで次のイベントを受け取らず、未処理イベントの溜まりすぎによるメモリ不足を防げる
-
制御フローが簡潔:
breakで終了でき、リスナー削除忘れのバグを防げる
5. 接続の維持(KeepAlive)
WebSocket接続は、音声データやメッセージが10秒間送信されないとタイムアウトします。話者が長時間沈黙している場合でも接続を維持するため、定期的にKeepAliveメッセージを送信する必要があります。
const KEEP_ALIVE_INTERVAL = 5000; // 5秒
let keepAliveTimer: NodeJS.Timeout;
connection.on(LiveTranscriptionEvents.Open, () => {
// KeepAliveを開始
keepAliveTimer = setInterval(() => {
if (connection.getReadyState() === SOCKET_STATES.open) {
connection.keepAlive();
}
}, KEEP_ALIVE_INTERVAL);
});
connection.on(LiveTranscriptionEvents.Close, () => {
// KeepAliveを停止
if (keepAliveTimer) {
clearInterval(keepAliveTimer);
}
});
ポイント:
- 10秒以内の間隔で
keepAlive()を呼び出す(ここでは5秒間隔) - 接続が開いている状態(
SOCKET_STATES.open)でのみ送信 - 接続終了時にタイマーをクリアする
6. 接続の切断
function disconnect() {
// KeepAliveタイマーを停止
if (keepAliveTimer) {
clearInterval(keepAliveTimer);
}
// 接続を閉じる
connection.requestClose();
}
知っておきたいこと
よく使うLive Transcription Options
interim_results(デフォルト: false)
- 目的: リアルタイムでの途中経過表示
- 用途: ユーザーに話している内容を即座にフィードバック
-
trueにすると、発話の途中でもis_final: falseの中間結果が送られてきます
diarize(デフォルト: false)
- 目的: 話者の識別
- 用途: 会議で誰がどの部分を話したかを区別
- 各単語に
speakerフィールド(話者番号)が付与されます
const speakerId = event.channel.alternatives[0].words[0]?.speaker;
// 結果例: 0, 1, 2... (話者ごとに番号が振られる)
endpointing(デフォルト: 10ms)
- 目的: 発話の終了を検出
- 用途: 発話の区切りを判定
- 指定したミリ秒間無音が続くと
speech_final: trueになります - 短い値(100-300ms): レスポンスが速いが、途中で区切られる可能性
- 長い値(500-1000ms): 文が完結しやすいが、レスポンスが遅い
smart_format / punctuate(デフォルト: false)
-
punctuate: 句読点の自動挿入(。、!?など) -
smart_format: 日付、時刻、数字、通貨などを読みやすく整形- 例: "one two three four" → "1234"
- 例: "january first" → "January 1st"
その他のオプション
全てのオプションは公式APIリファレンスを参照してください。
LiveTranscriptionEventsについて
Deepgramの接続で受信できる主なイベント:
| イベント名 | 発生タイミング | 説明 |
|---|---|---|
Open |
接続成功時 | WebSocket接続が確立された |
Transcript |
常に受信 | 文字起こし結果(中間・最終) |
Error |
エラー発生時 | エラー情報 |
Close |
接続終了時 | 接続が閉じられた |
SpeechStarted |
vad_events: true時 |
音声の開始を検出 |
UtteranceEnd |
utterance_end_ms設定時 |
発話の終了を検出 |
Metadata |
接続終了時 | セッション全体の統計情報 |
is_final と speech_final の違い
文字起こし結果には2つの重要なフラグがあります。これらの違いを理解することが重要です。
is_final: true
- 意味: Deepgramがその音声区間の処理を完了した
- 保証: 同じ時間範囲の文字起こし結果は二度と送信されない(確定)
- 用途: データベースへの保存、最終結果の表示
speech_final: true
- 意味: 発話の自然な区切りを検出した(無音/ポーズ)
-
トリガー:
endpointingで設定した無音時間が経過 - 用途: 発話の終わりを検知して処理を区切る
重要な関係性
// ✅ speech_final: true の場合
// → 必ず is_final: true も true
// ✅ is_final: true の場合
// → speech_final は false の可能性もある
つまり、長い発話の途中でも、Deepgramが区切りの良いところでis_final: trueを返すことがあります。
endpointing と utterance_end_ms の違い
発話の終了を検出する2つのオプションがあります。それぞれ異なる目的で使用します。
| オプション | 影響するフィールド/イベント | 用途 |
|---|---|---|
endpointing |
Transcriptのspeech_final
|
発話の終了を検出して確定 |
utterance_end_ms |
UtteranceEndイベント |
発話の完全終了を通知 |
図解
ユーザー: "こんにちは" [無音200ms] "今日は" [無音200ms] [無音1秒]
↓ ↓ ↓
Transcript Transcript UtteranceEnd
(speech_final) (speech_final)
endpointing: 200 → 200ms無音で speech_final: true
utterance_end_ms: 1000 → 1秒無音で UtteranceEnd イベント
使い分け
-
発話の区切りを細かく検出したい:
endpointingを短く(100-300ms) -
発話の完全な終了を通知させたい:
utterance_end_msを設定(500-2000ms) - 両方使う: 細かい区切り検出+完全終了通知の両方が可能
SOCKET_STATESの確認の重要性
音声データを送信する前に、必ずWebSocket接続が開いているか確認する必要があります。
import { SOCKET_STATES } from '@deepgram/sdk';
function sendAudioData(audioData: ArrayBuffer) {
// ✅ 接続状態を確認
if (connection.getReadyState() !== SOCKET_STATES.open) {
console.warn('接続が開いていないため、送信をスキップします');
return;
}
const uint8Array = new Uint8Array(audioData);
connection.send(uint8Array.buffer);
}
なぜ必要か
- 接続が確立される前にデータを送信するとエラーになる
- 接続が切断された後にデータを送信するとエラーになる
- エラーハンドリングを適切に行うことで、アプリケーションの安定性が向上
SOCKET_STATESの種類
SOCKET_STATES.connecting // 接続中
SOCKET_STATES.open // 接続完了(送信可能)
SOCKET_STATES.closing // 切断処理中
SOCKET_STATES.closed // 切断完了
最後に
この記事では、Deepgramのリアルタイム音声文字起こし機能について、実際の実装例をもとに紹介しました。
Deepgramにはリアルタイム音声文字起こし以外にも様々な機能があるので、みなさんも是非試してみてください!
参考文献
- Live Audio API Reference | Deepgram's Docs - Live Streaming用の全APIパラメータ
- Live Streaming Audio Guide | Deepgram's Docs - リアルタイム音声認識の実装ガイド
- deepgram-js-sdk - Live Transcription - JavaScript SDKの実装例
- Audio Keep Alive | Deepgram's Docs - KeepAliveの詳細
株式会社シンシア
株式会社xincereでは、実務未経験のエンジニアの方や学生エンジニアインターンを採用し一緒に働いています。
※ シンシアにおける働き方の様子はこちら
シンシアでは、年間100人程度の実務未経験の方が応募し技術面接を受けます。
その経験を通し、実務未経験者の方にぜひ身につけて欲しい技術力(文法)をここでは紹介していきます。