OpenAI, “How we built a realtime system for responsive voice AI in six months”(2026年8月3日)
OpenAIは音声を途切れさせないため、会話中の表示用ログと、あとから使う確定ログを分けて扱う設計を紹介しています。営業の通話メモでも、ここはそのまま真似した方がいいです。
リアルタイム文字起こしは、商談メモの入力に直結させません。途中の文字列には聞き直しや言い直しが混じるからです。先に確定した発言だけを残す。これだけで「予算は月10万円」が「予算は月」に化ける事故を止められます。
公式記事: https://openai.com/index/continuous-voice-interaction-with-gpt-live/
なぜ暫定版を残すと困るのか
音声の文字起こしは、話している最中に何度も書き換わります。画面に出すぶんには速い方が助かります。ただ、商談メモは別です。後続の担当者が読み、案件の予算や導入時期を拾い、場合によってはCRMへ転記します。
ここで暫定版と確定版を同じ表に追加すると、同じ発言が二重に残ります。訂正前の数字を誰かが先に見れば、それだけで面倒です。音声AIが賢くなるほど、表示が速いことと記録が正しいことは別の仕事になります。
ストリーミングの文字起こしは、最終版だけを商談メモに渡せば足ります。
JSON Linesから確定版だけをCSVにする
下のスクリプトは、文字起こし配信から受け取ったJSON Linesを標準入力で読みます。state が final の行だけを対象にし、同じ segment_id が更新されていたら version が大きいものを採用します。sequence で並べ直しているので、訂正イベントが後から届いても発言順は崩れません。
#!/usr/bin/env python3
"""Keep only the latest final version of each streamed transcript segment."""
import csv
import json
import sys
def latest_final_segments(lines):
latest = {}
for line_number, raw in enumerate(lines, start=1):
if not raw.strip():
continue
item = json.loads(raw)
if item.get("state") != "final":
continue
segment_id = item.get("segment_id")
version = item.get("version")
sequence = item.get("sequence")
text = item.get("text")
if not isinstance(segment_id, str) or not segment_id:
raise ValueError("line {}: segment_id is required".format(line_number))
if not isinstance(version, int):
raise ValueError("line {}: version must be an integer".format(line_number))
if not isinstance(sequence, int):
raise ValueError("line {}: sequence must be an integer".format(line_number))
if not isinstance(text, str) or not text.strip():
raise ValueError("line {}: text is required".format(line_number))
previous = latest.get(segment_id)
if previous is None or version > previous["version"]:
latest[segment_id] = item
return sorted(latest.values(), key=lambda item: item["sequence"])
def main():
writer = csv.DictWriter(sys.stdout, fieldnames=["sequence", "speaker", "text"])
writer.writeheader()
for item in latest_final_segments(sys.stdin):
writer.writerow({
"sequence": item["sequence"],
"speaker": item.get("speaker", ""),
"text": item["text"].strip(),
})
if __name__ == "__main__":
main()
入力は、たとえば次の形です。文字起こしサービスごとにキー名は違うので、受信側でこの4項目へ寄せます。partial は画面表示だけに使い、ファイルへ渡しません。
{"segment_id":"s-01","version":1,"sequence":1,"state":"partial","speaker":"顧客","text":"予算は月"}
{"segment_id":"s-02","version":1,"sequence":2,"state":"final","speaker":"営業","text":"導入希望はいつ頃ですか"}
{"segment_id":"s-01","version":2,"sequence":1,"state":"final","speaker":"顧客","text":"予算は月10万円です"}
{"segment_id":"s-01","version":3,"sequence":1,"state":"final","speaker":"顧客","text":"予算は月15万円です"}
実行はこうです。
python3 final_transcript_export.py < events.jsonl > meeting_notes.csv
今日、この4行をPython 3で流して確認した出力です。partial の「予算は月」は残らず、同じ発言の最新版だけになりました。
sequence,speaker,text
1,顧客,予算は月15万円です
2,営業,導入希望はいつ頃ですか
シートへ入れる前に決めること
CSVをスプレッドシートに取り込んだら、sequence、speaker、text の3列を元データとして残します。その横に担当者が「予算」「導入希望日」「次回アクション」を埋める列を作ると、文字起こしの訂正があっても判断用の列を壊しません。
一つだけ注意があります。音声認識が確定と返した文も、内容まで正しいとは限りません。金額、固有名詞、日付は録音か相手への確認メールで照合します。コードが守るのは、配信途中の文字列を記録へ混ぜないところまでです。
現場に置くなら
GPT-Liveのように会話を流しながら処理する仕組みが広がると、通話中の速さは当たり前になります。営業で差が出るのは、速く見えた文字をどこで確定させるかです。
まずは通話1本分のイベントをこの形式で保存し、既存の商談メモと見比べてください。partial と final を分けるだけで、後から読めるメモになります。ここ、地味に効きます。