完成した音声ファイルを文字起こしのエンドポイントにアップロードするのは、この問題の簡単なやり方です。ファイル全体を待ち、トランスクリプトをひとつ受け取るだけ。モデルが動いている間に誰かが画面を見つめる必要もありません。ライブ字幕は別の仕事です。手元の内容をどう扱うか決めている最中にも音声は届き続け、話者が話し続けている間にテキストが更新されなければなりません。
そのギャップを gpt-live-transcribeが埋めます。OpenAI は 2026年7月28日にこれをリリースし 、バッチ用の対になる gpt-transcribeも同時に公開しました。本チュートリアルでは、これを中心にPythonで字幕クライアントを作り、3つのテストを実行します。基本のストリーミングクライアント、受け付けるコンテキストヒントの比較、そして5つの遅延設定のベンチマークです。英語のクリアな音声、専門用語、そしてエジプト方言のアラビア語と英語のコードスイッチングでテストしました。きれいなナレーションひとつより現実の会議に近いからです。
最後まで読むと、動作する字幕アプリが手元に残り、実際に効くコンテキスト設定の勘所が分かり、当てずっぽうではなく根拠を持って選べる遅延設定が見つかります。
GPT Live Transcribeとは?
gpt-live-transcribeは、音声が届いている最中に文字起こし結果が必要なアプリケーション向けのストリーミング型音声認識モデルです。入力は音声、出力はテキストのみで、調整できるのは4つのフィールド: delay(レイテンシ)、 prompt(自由形式のコンテキスト)、keywords(そのままの用語)、languages(想定入力言語)です。OpenAIのContext Aware ASRベンチマークでは、自由形式のコンテキストにより意味的な正確さが38.5%から44.6%へ上がりました。これがテスト2を用意した理由です。
このモデルはRealtime API の中で動作し、別エンドポイントではありません。名前が何を示唆しようと、OpenAIの音声システムであるGPT-Liveとは無関係です。文字起こしセッションを開き、設定し、音声を送るのと同じ接続でサーバーからイベントがストリームバックされます。まず決めるべきは、2つの文字起こしモデルのうち本当に必要なのはどちらか、という点です。
GPT Live Transcribe と GPT Transcribe の違い
OpenAIは推奨モデルを2つ出しており、互換ではありません。gpt-live-transcribeはマイクや電話、メディアストリームのように音声が継続的に到着し、話者が話し終える前に部分的なテキストが必要なときに使います。gpt-transcribeは録音が完了している場合、あるいはRealtimeセッションで意図的に確定ターンを待つ場合に使います。ドキュメントでは後者を特殊なワークフロー と呼んでおり、ライブの差分を得る方法ではありません。
ひとつ混乱しやすい違いがあります。gpt-transcribeは検出した入力言語を languages配列で返しますが、 gpt-live-transcribeは返しません。検出言語に応じてロジックを分岐させるなら、デモで字幕がどれだけ良く見えてもそのモデルは不適切です。料金も同じ線で分かれ、バッチモデルのほうが概ね4倍お得です。この点には後で戻ります。
gpt-live-transcribe が返さないもの
アプリの半分を作ってから知るより先に伝えておきます。単語レベルのタイムスタンプ、話者ラベル、信頼度スコア、話者分離はありません。字幕のタイミング、誰が話したかのメモ、信頼度のしきい値が必要なら、OpenAIのガイドは gpt-4o-transcribe-diarizeか whisper-1を指しています。
PythonでGPT Live Transcribeをセットアップする
本チュートリアルのスクリプトはすべて github.com/KhalidAbdelaty/gpt-live-transcribe にあります。まずはクローンしてください。必要なのはPython 3.10以降とRealtimeアクセス権のあるAPIキーです。スクリプト自体は4つのパッケージに依存します: websockets (接続用)、 sounddevice (マイク入力)、 numpy (バッファ変換)、 python-dotenv(キー読み込み)。requirementsにはグラフやブラウザデモ用のパッケージも少し入っています。
git clone https://github.com/KhalidAbdelaty/gpt-live-transcribe.git
cd gpt-live-transcribe
pip install -r requirements.txt提供:
macOSではsounddevice にOSレベルのPortAudioが必要です( brew install portaudio)。Linuxでは apt-get install portaudio19-devです。Windowsならこの行は不要です。私自身もmacOSでこの問題に当たり、修正は本当にこの1回のインストールだけでした。
音声は16-bit PCM、24 kHz、モノラル、リトルエンディアン、base64エンコードで送る必要があります。MP3やステレオのWAVを送ると、出力が文字化けしたり接続が閉じられたりしますが、形式が間違っているとは教えてくれません。午後が消える落とし穴です。次は、どの接続で音声を運ぶかです。
WebSocket と WebRTC の選択
OpenAIのガイダンスは明確です。サーバー間はWebSocket、ブラウザやモバイルはWebRTC。本チュートリアルはローカルのマイクを読むPythonバックエンドを作るのでWebSocketが正解で、APIキーはサーバー外に出ないため通常のキーで問題ありません。
セッションとイベントフローの理解
セッションはまずsession.updateイベントで始まり、type: \"transcription\"を設定し、モデルに gpt-live-transcribeを指定します。ペイロードの残りは、これから送る音声の記述です。以下はRealtime文字起こしガイドにある最小構成です。
session_config = {
"type" : "session.update" ,
"session" : {
"type" : "transcription" ,
"audio" : {
"input" : {
"format" : { "type" : "audio/pcm" , "rate" : 24000 } ,
"transcription" : { "model" : "gpt-live-transcribe" } ,
"turn_detection" : None ,
}
} ,
} ,
} 提供:
turn_detection: Noneは自動の音声区間検出を無効にします。よって明示的にコミットしない限り確定しません。以降の3つのクライアントイベントが仕事をします: input_audio_buffer.append(base64の音声チャンク送信)、 input_audio_buffer.commit(ターン終了)、サーバーからは conversation.item.input_audio_transcription.delta(部分テキスト)と conversation.item.input_audio_transcription.completed(最終テキスト)が返ります。接続先は wss://api.openai.com/v1/realtime?intent=transcriptionで、これはOpenAIのクックブックにあるパターンです。ガイドにはそのクエリ文字列が記載されていないので、もし将来動かなくなったら外してください。
Realtime文字起こしセッションのイベントフローダイアグラム。画像:筆者作成。
基本的なライブ文字起こしクライアントを作る
テスト1は最小構成です。マイク音声を取り込み、ストリームし、到着した部分テキストと最終テキストを出力します。コンテキストもキーワードも調整もなし。イベントフローを見通せるようにします。最初の課題は、マイクのスレッドを止めずに音声を取り出すことです。
マイク音声のストリーミング
sounddeviceは数ミリ秒以内に返さないとドライバがフレームを落とす独自スレッドでコールバックを走らせるため、ネットワーク待ちはできません。仕事はひとつ、float32のバッファをPCM16に変換して asyncio.Queue に loop.call_soon_threadsafeで投げること。別のコルーチンがそのキューをドレインしてチャンクを送信します。
def callback ( indata, frames, time_info, status) :
pcm16 = ( indata[ : , 0 ] * 32767 ) . astype( np. int16) . tobytes( )
loop. call_soon_threadsafe( queue. put_nowait, pcm16)
stream = sd. InputStream( samplerate= 24000 , channels= 1 , dtype= "float32" ,
blocksize= 2400 , callback= callback) 提供:
100ミリ秒のチャンク(24 kHzで2,400サンプル)は妥当な出発点です。小さくしすぎるとメッセージ単位のオーバーヘッドが増え、大きすぎると字幕がもたつきます。正解の数値は文書化されていないので、ダイヤルとして扱ってください。
部分テキストと最終テキストの処理
デルタは軽くて頻繁に来ます。各デルタはitem_idに紐づくテキスト片を持ちます。同じアイテムの既存の部分テキストにそれを連結すると、画面上の字幕が単語ごとに伸びていきます。
if event[ "type" ] == "conversation.item.input_audio_transcription.delta" :
item_id = event[ "item_id" ]
partials[ item_id] = partials. get( item_id, "" ) + event[ "delta" ]
print ( f"\r[partial] { partials[ item_id] } " , end= "" ) 提供:
completedイベントは、そのアイテムの確定トランスクリプトで部分テキストを置き換えます。completedを真実のソースとして扱い、デルタはあくまでプレビューであり、自分でつなぎ合わせるものではない、と考えてください。
item_idでトランスクリプト状態を管理する
ここを飛ばすとUIが壊れます。OpenAIのガイドには、異なるターンの完了イベント同士の順序は保証されないとあります。早いターンのcompletedが遅れて届くことがあるため、最新の completedが最新ターンだと仮定すると、たまに逆戻りしたり行が重複したりします。そこで item_idで全てをキーにするのが正解です。私の TranscriptStateクラスがそれを行います。
この実装で2つのバグを捕まえました。どの辞書を見ているかという問題です。item_idを順序リストに追加するのをデルタハンドラだけでやると、 full_transcript()は completedイベントだけを処理する受信側では空になりました。新規アイテムかどうかの判定に部分テキストの辞書を使うのはもっと悪手です。 apply_completed() がそのエントリを消すので、遅れてきたデルタが新規に見え、順序リストに2回入って、同じターンを2回出力しました。両方のハンドラで item_idを追跡し、順序リストを確認してください。
ライブの部分字幕が確定してトランスクリプトになる様子。画像:筆者作成。
クリアな英語では、1〜2秒でテキストが現れ、発話どおり(句読点込み)でした。ただし、ノートPCのマイクでコンテキストなしだと、不確かな単語がまったく別の文字体系で返ることが時々ありました。 languagesがそのためのフィールドで、これがテスト2の狙いです。
コンテキストとキーワードで精度を上げる
モデルは3種類のコンテキストを受け付けます。テスト前に正確に押さえておきましょう。 promptは状況を記述する自由形式テキスト、 keywordsは音声に含まれうる文字通りの用語、 languagesは enや arのようなISO 639-1コードで想定入力言語を指定します。どれも出力を強制はしません。実際に話していないキーワードは、列挙しただけでは現れません。これらのフィールドが何をするかを知る唯一の方法は、一度にひとつずつ変えて確かめることです。
prompt、keywords、languagesヒントのテスト
同じクリップを5つの構成で、各3回ずつ走らせました。こうした比較の公正さを担保するルールは2つ。1回の実行で変えるコンテキストフィールドは1つだけ、各構成は複数回走らせる。モデルは同一音声でも決定論的ではないからです。
RUNS = {
"no_context" : TranscriptionConfig( delay= "low" ) ,
"prompt_only" : TranscriptionConfig( delay= "low" , prompt= PROMPT) ,
"keywords_only" : TranscriptionConfig( delay= "low" , keywords= KEYWORDS) ,
"languages_only" : TranscriptionConfig( delay= "low" , languages= [ "en" ] ) ,
"prompt_and_keywords" : TranscriptionConfig(
delay= "low" , prompt= PROMPT, keywords= KEYWORDS,
) ,
} 提供:
最初の版では、複合の実行だけにlanguagesを設定しており、これは1度に2つのフィールドを変えてしまうためルール違反でした。もうひとつ、フォーマットのルールでセッション更新が弾かれたことがあります。<、 >、復帰(CR)や改行(LF)を含むキーワードが1つでもあると、当該キーワードだけでなく更新全体が拒否されます。TranscriptionConfig.validate_keywords()は、ペイロードを組み立てる前にそれを検出します。
コンテキストが直したこと・直せなかったこと
キーワードは、予想どおりの種類の音声で効きました。どの実行でも、口頭で述べられた口座番号は音声どおりの単語として書き起こされます。問題は、モデルがそれらの単語を識別子としてまとめるか、「A C 42」のように文字を区切って綴るかでした。
15回の実行で分布は明確でした。 keywordsなしでは一度もまとまりません。 no_context、 prompt_only、 languages_onlyの9回はすべて「A C 42」という結果でした。 keywordsありでは6回中5回でまとまり、ほとんどは「AC-42」という最終形式でした。つまりkeywordsが結果を動かし、 prompt単独では動かない、というOpenAIの位置づけ通りです。もっとも、キーワードだけでも1回は外しました。したがって、モデルが従う規則ではなく、強く確率を傾けるヒントだと捉えてください。
これは、冒頭に挙げたベンチマークの数字(自由形式のコンテキストが意味的正確さを6ポイント押し上げた)と並べると落ち着きが悪いかもしれません。測っているものが違います。OpenAIは広い音声集合で意味を評価し、私はひとつのクリップの1つの識別子だけを見ました。 promptが文全体で実際に効いていても、たまたまチェックしている狭い箇所には影響しないことがあります。ここでそれを示す唯一の兆候は、 promptとkeywordsの併用では毎回まとまり、keywords単独では1回外した、という、1回分の差だけです。
識別子をまとめられたのはキーワードのみ。画像:筆者作成。
言語ヒントは、予想外の問題に対してより明確に効きました。ヒントなしでは、コードスイッチングのクリップで、冒頭のアラビア語のつなぎ言葉(概ね「タイイェブ」)を英語の「But」として聞き取り、2つの文字体系を融合させた壊れた単語を作っていました。また「billing statement」をアラビア文字で音写する場合とラテン文字のままの場合がありました。 languages: [\"ar\", \"en\"]を追加すると、壊れた単語は毎回消えました。とはいえこれは1つのクリップで、文の途中で言語が切り替わる文はヒントが最も効きやすい場面です。
5つの遅延レベルのベンチマーク
delayは minimal, low, medium, high, xhigh の5段階です。低い設定は部分テキストを早く出せます。高い設定は確定前により多くの音声コンテキストを与えるため、難しい音声で精度が改善することがあります。正確なタイミングは構成により異なり、代表的な音声でベンチマークすべきだとOpenAIは明記しています。そこでテスト3を行います。
ベンチマークの実行
test3_delay_benchmark.pyは同じWAVファイルを5レベルすべてで複数回ストリーミングし、ストリーム開始から最初のデルタ、そして最終トランスクリプトまでの時間を記録します。音声、コンテキストフィールド、コミット戦略を同一に保つことが比較を意味のあるものにします。
async def benchmark_once ( delay: str , wav_path: str ) - > dict :
config = TranscriptionConfig( delay= delay)
return {
"delay" : delay,
"time_to_first_delta_s" : first_delta_at - start,
"time_to_final_s" : final_at - start,
"delta_event_count" : delta_count,
} 提供:
結果が示したこと
これらの数値は普遍ではありません。ひとつのクリップ、ひとつのネットワーク、同じ午後に各レベル3回ずつの結果です。最初の部分までの中央値は、minimalが0.70秒、 xhighが2.91秒で、 low(1.19秒)、 medium(1.39秒)、 high(2.09秒)と段階的に伸びました。各レベル内での3回のばらつきは0.2秒程度で、値は私の環境依存でも順序は安定しています。
遅延レベルは速度と精度のトレードオフ。画像:筆者作成。
確認できなかったのは、「遅延が高いほど修正が減る」という通念です。デルタの件数は各レベルで84〜86の範囲に収まり、傾向は見えませんでした。最終までの時間もどこでも30.6秒±0.5秒の範囲でしたが、これはモデルではなく私のコミットタイミングの反映です。チャートを2つのパネルに分けたのはそのためです。一方の軸では2秒の差が、10倍の高さの棒の下で消えてしまいます。
用途に合わせた遅延の選び方
話者の発話に合わせて誰かが読むライブ字幕には、まず lowから。2秒待っても何も出ないのは、直後に字幕が修正されるよりも壊れて見えます。後から読む議事メモなら、 high または xhigh でコストはほぼありません。音声コマンドなら medium寄り。2語のコマンドでは、1語の誤りが通常以上に響きます。
ターン検出と音声コミットの扱い
ここまでのテストはすべて turn_detection: null と手動コミットでした。Realtime APIは代替として音声区間検出を提供しているので、 gpt-live-transcribeに対して実際に配線してみました。動くだろうと決めつけるのではなく。テストが失敗した時点でこの節を削ろうとしましたが、その失敗こそが発見でした。
手動コミット vs. 音声区間検出
server_vadは無音区間で音声を分割し、 threshold、 prefix_padding_ms、 silence_duration_msで調整可能です。 semantic_vadは話者が話し終えたかを推定する分類器を使い、 eagernessで決断の早さを制御します。
"turn_detection" : { "type" : "semantic_vad" , "eagerness" : "auto" } 提供:
これはRealtime API全般でのドキュメントどおりです。これをgpt-live-transcribeのセッションに送ると、そのまま拒否されます。
{
"type" : "error" ,
"error" : {
"type" : "invalid_request_error" ,
"code" : "invalid_value" ,
"message" : "Turn detection is not supported for this transcription model." ,
"param" : "session.audio.input.turn_detection"
}
} 提供:
server_vadでも同一のエラーでした。2026年8月4日現在、 gpt-live-transcribeが受け付けるターン検出は手動コミットのみです。文字起こしガイドには今もサーバーにコミットさせるため音声区間検出を設定するよう書かれているにもかかわらず。今後OpenAIが告知なく本モデルでVADを有効にする可能性もあるので、構築前に必ず再テストしてください。
ターン戦略の選択
プッシュ・トゥ・トークは簡単です。押下と解放がすでに境界を示すからです。それ以外は、クライアントがターンの終了を判断します。最初の2つの試みはいずれも外れました。
試み1は、 if not mic_queue.empty()でコミットをガードしました。一見もっともらしいのですが、決して発火しません。キューをドレインするコルーチンが、マイクが満たすのと同じ速さで空にしてしまうからです。部分字幕は流れるため、もっともらしく見えますが、何も確定しません。試み2は、音声が追加されている限り4秒ごとにコミットしました。実マイク相手だと結果はこうなります。
[final] This is a customer support (item_id=item_E8bCJcvjO9L1KU2zckqOr)
[final] Abort call about the premium plan on account A (item_id=item_E8bCNSu1dmYK380JOr1ro)
[final] (item_id=item_E8bCVgxGSjXTasbrTWE3U)提供:
2つの失敗が同時に起きました。タイマーが単語の途中で文を切り、どこからも始まっていない断片をモデルが「Abort」と読みました。さらに、話していない間にコミットして空のトランスクリプトを返しました。マイクは誰かが話していようといまいとチャンクをストリームするからです。
いずれも足りない情報、すなわち音声エネルギーに起因します。mic_stream.pyのSpeechGateは各チャンクのRMS振幅を追跡し、話者が発話してから静かになったらコミットします。連続発話でもどこかで終える上限を設けています。最初の版では固定値と比較しており、あるマシンでは動いても次のマシンでは5倍ずれていました。今は部屋のノイズを推定し、その数倍を音声とみなします。ほとんど無音(せきやドア)で終わるターンは、コミットではなく input_audio_buffer.clearに回します。ドアが何と言ったかをモデルに尋ねるのは、でっち上げの単語を得る近道だからです。
完成版ライブ字幕アプリを作る
app.py は本チュートリアルの全要素をひとつのターミナルアプリにまとめます。マイク取得、ライブの部分字幕、 item_idでキー付けした履歴、セッションが受け付けるすべてのフィールド用のCLIフラグです。
python app.py --delay low --keywords "AC-42,premium plan" --languages en提供:
実際に話す言語だけを指定してください。英語だけの発話に対して同じコマンドでen,arを与えると、「delta」という単語がアラビア文字に音写されました。テスト2の逆方向の所見です。
--turn-detectionはデフォルトで manual、--silence-hold と --max-turnは前節のゲートを調整します。APIが受け付け始める可能性に備えてVADモードもフラグとして残してあります。渡すと、ハングせずサーバーからの拒否を表示します。
設定付きで動作する完成版字幕アプリ。画像:筆者作成。
終了時にはプレーンテキストのトランスクリプトと、使用した構成、ローカルで測った最初のデルタまでの時間、各確定ターンとその item_idを含むJSONファイルを書き出します。良いテスト走行を閉じたターミナルで失った反省から追加しました。タイムスタンプはクライアント側なので、自分の計測をOpenAIの数値と取り違えないでください。
3つのテストはブラウザでも動きます。 demo_app.pyは Streamlit 版で、各実験ごとにタブがあります。メインの学習経路ではなくデモとして残しています。ターミナルのスクリプトのほうが生のイベントを直接示せるからです。
streamlit run demo_app.py提供:
完成した音声ファイルを文字起こしのエンドポイントにアップロードするのは、この問題の簡単なやり方です。ファイル全体を待ち、トランスクリプトをひとつ受け取るだけ。モデルが動いている間に誰かが画面を見つめる必要もありません。ライブ字幕は別の仕事です。手元の内容をどう扱うか決めている最中にも音声は届き続け、話者が話し続けている間にテキストが更新されなければなりません。
そのギャップを
gpt-live-transcribeが埋めます。OpenAI は2026年7月28日にこれをリリースし、バッチ用の対になるgpt-transcribeも同時に公開しました。本チュートリアルでは、これを中心にPythonで字幕クライアントを作り、3つのテストを実行します。基本のストリーミングクライアント、受け付けるコンテキストヒントの比較、そして5つの遅延設定のベンチマークです。英語のクリアな音声、専門用語、そしてエジプト方言のアラビア語と英語のコードスイッチングでテストしました。きれいなナレーションひとつより現実の会議に近いからです。最後まで読むと、動作する字幕アプリが手元に残り、実際に効くコンテキスト設定の勘所が分かり、当てずっぽうではなく根拠を持って選べる遅延設定が見つかります。
GPT Live Transcribeとは?
gpt-live-transcribeは、音声が届いている最中に文字起こし結果が必要なアプリケーション向けのストリーミング型音声認識モデルです。入力は音声、出力はテキストのみで、調整できるのは4つのフィールド:delay(レイテンシ)、prompt(自由形式のコンテキスト)、keywords(そのままの用語)、languages(想定入力言語)です。OpenAIのContext Aware ASRベンチマークでは、自由形式のコンテキストにより意味的な正確さが38.5%から44.6%へ上がりました。これがテスト2を用意した理由です。このモデルはRealtime APIの中で動作し、別エンドポイントではありません。名前が何を示唆しようと、OpenAIの音声システムであるGPT-Liveとは無関係です。文字起こしセッションを開き、設定し、音声を送るのと同じ接続でサーバーからイベントがストリームバックされます。まず決めるべきは、2つの文字起こしモデルのうち本当に必要なのはどちらか、という点です。
GPT Live Transcribe と GPT Transcribe の違い
OpenAIは推奨モデルを2つ出しており、互換ではありません。
gpt-live-transcribeはマイクや電話、メディアストリームのように音声が継続的に到着し、話者が話し終える前に部分的なテキストが必要なときに使います。gpt-transcribeは録音が完了している場合、あるいはRealtimeセッションで意図的に確定ターンを待つ場合に使います。ドキュメントでは後者を特殊なワークフローと呼んでおり、ライブの差分を得る方法ではありません。ひとつ混乱しやすい違いがあります。
gpt-transcribeは検出した入力言語をlanguages配列で返しますが、gpt-live-transcribeは返しません。検出言語に応じてロジックを分岐させるなら、デモで字幕がどれだけ良く見えてもそのモデルは不適切です。料金も同じ線で分かれ、バッチモデルのほうが概ね4倍お得です。この点には後で戻ります。gpt-live-transcribe が返さないもの
アプリの半分を作ってから知るより先に伝えておきます。単語レベルのタイムスタンプ、話者ラベル、信頼度スコア、話者分離はありません。字幕のタイミング、誰が話したかのメモ、信頼度のしきい値が必要なら、OpenAIのガイドは
gpt-4o-transcribe-diarizeかwhisper-1を指しています。PythonでGPT Live Transcribeをセットアップする
本チュートリアルのスクリプトはすべてgithub.com/KhalidAbdelaty/gpt-live-transcribeにあります。まずはクローンしてください。必要なのはPython 3.10以降とRealtimeアクセス権のあるAPIキーです。スクリプト自体は4つのパッケージに依存します:
websockets(接続用)、sounddevice(マイク入力)、numpy(バッファ変換)、python-dotenv(キー読み込み)。requirementsにはグラフやブラウザデモ用のパッケージも少し入っています。macOSでは
sounddeviceにOSレベルのPortAudioが必要です(brew install portaudio)。Linuxではapt-get install portaudio19-devです。Windowsならこの行は不要です。私自身もmacOSでこの問題に当たり、修正は本当にこの1回のインストールだけでした。音声は16-bit PCM、24 kHz、モノラル、リトルエンディアン、base64エンコードで送る必要があります。MP3やステレオのWAVを送ると、出力が文字化けしたり接続が閉じられたりしますが、形式が間違っているとは教えてくれません。午後が消える落とし穴です。次は、どの接続で音声を運ぶかです。
WebSocket と WebRTC の選択
OpenAIのガイダンスは明確です。サーバー間はWebSocket、ブラウザやモバイルはWebRTC。本チュートリアルはローカルのマイクを読むPythonバックエンドを作るのでWebSocketが正解で、APIキーはサーバー外に出ないため通常のキーで問題ありません。
セッションとイベントフローの理解
セッションはまず
session.updateイベントで始まり、type: \"transcription\"を設定し、モデルにgpt-live-transcribeを指定します。ペイロードの残りは、これから送る音声の記述です。以下はRealtime文字起こしガイドにある最小構成です。turn_detection: Noneは自動の音声区間検出を無効にします。よって明示的にコミットしない限り確定しません。以降の3つのクライアントイベントが仕事をします:input_audio_buffer.append(base64の音声チャンク送信)、input_audio_buffer.commit(ターン終了)、サーバーからはconversation.item.input_audio_transcription.delta(部分テキスト)とconversation.item.input_audio_transcription.completed(最終テキスト)が返ります。接続先はwss://api.openai.com/v1/realtime?intent=transcriptionで、これはOpenAIのクックブックにあるパターンです。ガイドにはそのクエリ文字列が記載されていないので、もし将来動かなくなったら外してください。Realtime文字起こしセッションのイベントフローダイアグラム。画像:筆者作成。
基本的なライブ文字起こしクライアントを作る
テスト1は最小構成です。マイク音声を取り込み、ストリームし、到着した部分テキストと最終テキストを出力します。コンテキストもキーワードも調整もなし。イベントフローを見通せるようにします。最初の課題は、マイクのスレッドを止めずに音声を取り出すことです。
マイク音声のストリーミング
sounddeviceは数ミリ秒以内に返さないとドライバがフレームを落とす独自スレッドでコールバックを走らせるため、ネットワーク待ちはできません。仕事はひとつ、float32のバッファをPCM16に変換してasyncio.Queueにloop.call_soon_threadsafeで投げること。別のコルーチンがそのキューをドレインしてチャンクを送信します。100ミリ秒のチャンク(24 kHzで2,400サンプル)は妥当な出発点です。小さくしすぎるとメッセージ単位のオーバーヘッドが増え、大きすぎると字幕がもたつきます。正解の数値は文書化されていないので、ダイヤルとして扱ってください。
部分テキストと最終テキストの処理
デルタは軽くて頻繁に来ます。各デルタは
item_idに紐づくテキスト片を持ちます。同じアイテムの既存の部分テキストにそれを連結すると、画面上の字幕が単語ごとに伸びていきます。completedイベントは、そのアイテムの確定トランスクリプトで部分テキストを置き換えます。completedを真実のソースとして扱い、デルタはあくまでプレビューであり、自分でつなぎ合わせるものではない、と考えてください。item_idでトランスクリプト状態を管理する
ここを飛ばすとUIが壊れます。OpenAIのガイドには、異なるターンの完了イベント同士の順序は保証されないとあります。早いターンの
completedが遅れて届くことがあるため、最新のcompletedが最新ターンだと仮定すると、たまに逆戻りしたり行が重複したりします。そこでitem_idで全てをキーにするのが正解です。私のTranscriptStateクラスがそれを行います。この実装で2つのバグを捕まえました。どの辞書を見ているかという問題です。
item_idを順序リストに追加するのをデルタハンドラだけでやると、full_transcript()はcompletedイベントだけを処理する受信側では空になりました。新規アイテムかどうかの判定に部分テキストの辞書を使うのはもっと悪手です。apply_completed()がそのエントリを消すので、遅れてきたデルタが新規に見え、順序リストに2回入って、同じターンを2回出力しました。両方のハンドラでitem_idを追跡し、順序リストを確認してください。ライブの部分字幕が確定してトランスクリプトになる様子。画像:筆者作成。
クリアな英語では、1〜2秒でテキストが現れ、発話どおり(句読点込み)でした。ただし、ノートPCのマイクでコンテキストなしだと、不確かな単語がまったく別の文字体系で返ることが時々ありました。
languagesがそのためのフィールドで、これがテスト2の狙いです。コンテキストとキーワードで精度を上げる
モデルは3種類のコンテキストを受け付けます。テスト前に正確に押さえておきましょう。
promptは状況を記述する自由形式テキスト、keywordsは音声に含まれうる文字通りの用語、languagesはenやarのようなISO 639-1コードで想定入力言語を指定します。どれも出力を強制はしません。実際に話していないキーワードは、列挙しただけでは現れません。これらのフィールドが何をするかを知る唯一の方法は、一度にひとつずつ変えて確かめることです。prompt、keywords、languagesヒントのテスト
同じクリップを5つの構成で、各3回ずつ走らせました。こうした比較の公正さを担保するルールは2つ。1回の実行で変えるコンテキストフィールドは1つだけ、各構成は複数回走らせる。モデルは同一音声でも決定論的ではないからです。
最初の版では、複合の実行だけに
languagesを設定しており、これは1度に2つのフィールドを変えてしまうためルール違反でした。もうひとつ、フォーマットのルールでセッション更新が弾かれたことがあります。<、>、復帰(CR)や改行(LF)を含むキーワードが1つでもあると、当該キーワードだけでなく更新全体が拒否されます。TranscriptionConfig.validate_keywords()は、ペイロードを組み立てる前にそれを検出します。コンテキストが直したこと・直せなかったこと
キーワードは、予想どおりの種類の音声で効きました。どの実行でも、口頭で述べられた口座番号は音声どおりの単語として書き起こされます。問題は、モデルがそれらの単語を識別子としてまとめるか、「A C 42」のように文字を区切って綴るかでした。
15回の実行で分布は明確でした。
keywordsなしでは一度もまとまりません。no_context、prompt_only、languages_onlyの9回はすべて「A C 42」という結果でした。keywordsありでは6回中5回でまとまり、ほとんどは「AC-42」という最終形式でした。つまりkeywordsが結果を動かし、prompt単独では動かない、というOpenAIの位置づけ通りです。もっとも、キーワードだけでも1回は外しました。したがって、モデルが従う規則ではなく、強く確率を傾けるヒントだと捉えてください。これは、冒頭に挙げたベンチマークの数字(自由形式のコンテキストが意味的正確さを6ポイント押し上げた)と並べると落ち着きが悪いかもしれません。測っているものが違います。OpenAIは広い音声集合で意味を評価し、私はひとつのクリップの1つの識別子だけを見ました。
promptが文全体で実際に効いていても、たまたまチェックしている狭い箇所には影響しないことがあります。ここでそれを示す唯一の兆候は、promptとkeywordsの併用では毎回まとまり、keywords単独では1回外した、という、1回分の差だけです。識別子をまとめられたのはキーワードのみ。画像:筆者作成。
言語ヒントは、予想外の問題に対してより明確に効きました。ヒントなしでは、コードスイッチングのクリップで、冒頭のアラビア語のつなぎ言葉(概ね「タイイェブ」)を英語の「But」として聞き取り、2つの文字体系を融合させた壊れた単語を作っていました。また「billing statement」をアラビア文字で音写する場合とラテン文字のままの場合がありました。
languages: [\"ar\", \"en\"]を追加すると、壊れた単語は毎回消えました。とはいえこれは1つのクリップで、文の途中で言語が切り替わる文はヒントが最も効きやすい場面です。5つの遅延レベルのベンチマーク
delayはminimal,low,medium,high,xhighの5段階です。低い設定は部分テキストを早く出せます。高い設定は確定前により多くの音声コンテキストを与えるため、難しい音声で精度が改善することがあります。正確なタイミングは構成により異なり、代表的な音声でベンチマークすべきだとOpenAIは明記しています。そこでテスト3を行います。ベンチマークの実行
test3_delay_benchmark.pyは同じWAVファイルを5レベルすべてで複数回ストリーミングし、ストリーム開始から最初のデルタ、そして最終トランスクリプトまでの時間を記録します。音声、コンテキストフィールド、コミット戦略を同一に保つことが比較を意味のあるものにします。結果が示したこと
これらの数値は普遍ではありません。ひとつのクリップ、ひとつのネットワーク、同じ午後に各レベル3回ずつの結果です。最初の部分までの中央値は、
minimalが0.70秒、xhighが2.91秒で、low(1.19秒)、medium(1.39秒)、high(2.09秒)と段階的に伸びました。各レベル内での3回のばらつきは0.2秒程度で、値は私の環境依存でも順序は安定しています。遅延レベルは速度と精度のトレードオフ。画像:筆者作成。
確認できなかったのは、「遅延が高いほど修正が減る」という通念です。デルタの件数は各レベルで84〜86の範囲に収まり、傾向は見えませんでした。最終までの時間もどこでも30.6秒±0.5秒の範囲でしたが、これはモデルではなく私のコミットタイミングの反映です。チャートを2つのパネルに分けたのはそのためです。一方の軸では2秒の差が、10倍の高さの棒の下で消えてしまいます。
用途に合わせた遅延の選び方
話者の発話に合わせて誰かが読むライブ字幕には、まず
lowから。2秒待っても何も出ないのは、直後に字幕が修正されるよりも壊れて見えます。後から読む議事メモなら、highまたはxhighでコストはほぼありません。音声コマンドならmedium寄り。2語のコマンドでは、1語の誤りが通常以上に響きます。ターン検出と音声コミットの扱い
ここまでのテストはすべて
turn_detection: nullと手動コミットでした。Realtime APIは代替として音声区間検出を提供しているので、gpt-live-transcribeに対して実際に配線してみました。動くだろうと決めつけるのではなく。テストが失敗した時点でこの節を削ろうとしましたが、その失敗こそが発見でした。手動コミット vs. 音声区間検出
server_vadは無音区間で音声を分割し、threshold、prefix_padding_ms、silence_duration_msで調整可能です。semantic_vadは話者が話し終えたかを推定する分類器を使い、eagernessで決断の早さを制御します。これはRealtime API全般でのドキュメントどおりです。これを
gpt-live-transcribeのセッションに送ると、そのまま拒否されます。server_vadでも同一のエラーでした。2026年8月4日現在、gpt-live-transcribeが受け付けるターン検出は手動コミットのみです。文字起こしガイドには今もサーバーにコミットさせるため音声区間検出を設定するよう書かれているにもかかわらず。今後OpenAIが告知なく本モデルでVADを有効にする可能性もあるので、構築前に必ず再テストしてください。ターン戦略の選択
プッシュ・トゥ・トークは簡単です。押下と解放がすでに境界を示すからです。それ以外は、クライアントがターンの終了を判断します。最初の2つの試みはいずれも外れました。
試み1は、
if not mic_queue.empty()でコミットをガードしました。一見もっともらしいのですが、決して発火しません。キューをドレインするコルーチンが、マイクが満たすのと同じ速さで空にしてしまうからです。部分字幕は流れるため、もっともらしく見えますが、何も確定しません。試み2は、音声が追加されている限り4秒ごとにコミットしました。実マイク相手だと結果はこうなります。2つの失敗が同時に起きました。タイマーが単語の途中で文を切り、どこからも始まっていない断片をモデルが「Abort」と読みました。さらに、話していない間にコミットして空のトランスクリプトを返しました。マイクは誰かが話していようといまいとチャンクをストリームするからです。
いずれも足りない情報、すなわち音声エネルギーに起因します。
mic_stream.pyのSpeechGateは各チャンクのRMS振幅を追跡し、話者が発話してから静かになったらコミットします。連続発話でもどこかで終える上限を設けています。最初の版では固定値と比較しており、あるマシンでは動いても次のマシンでは5倍ずれていました。今は部屋のノイズを推定し、その数倍を音声とみなします。ほとんど無音(せきやドア)で終わるターンは、コミットではなくinput_audio_buffer.clearに回します。ドアが何と言ったかをモデルに尋ねるのは、でっち上げの単語を得る近道だからです。完成版ライブ字幕アプリを作る
app.pyは本チュートリアルの全要素をひとつのターミナルアプリにまとめます。マイク取得、ライブの部分字幕、item_idでキー付けした履歴、セッションが受け付けるすべてのフィールド用のCLIフラグです。実際に話す言語だけを指定してください。英語だけの発話に対して同じコマンドで
en,arを与えると、「delta」という単語がアラビア文字に音写されました。テスト2の逆方向の所見です。--turn-detectionはデフォルトでmanual、--silence-holdと--max-turnは前節のゲートを調整します。APIが受け付け始める可能性に備えてVADモードもフラグとして残してあります。渡すと、ハングせずサーバーからの拒否を表示します。設定付きで動作する完成版字幕アプリ。画像:筆者作成。
終了時にはプレーンテキストのトランスクリプトと、使用した構成、ローカルで測った最初のデルタまでの時間、各確定ターンとその
item_idを含むJSONファイルを書き出します。良いテスト走行を閉じたターミナルで失った反省から追加しました。タイムスタンプはクライアント側なので、自分の計測をOpenAIの数値と取り違えないでください。3つのテストはブラウザでも動きます。
demo_app.pyはStreamlit版で、各実験ごとにタブがあります。メインの学習経路ではなくデモとして残しています。ターミナルのスクリプトのほうが生のイベントを直接示せるからです。