0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Zoom AI Services 実装編】商談音声を Speech Summarization × Decisions でパイプライン化する — 会話の途中で条件が変わっても、ちゃんと拾えるか?

0
Posted at

この記事は「Zoom AI Services を触り倒すシリーズ」の 2本目 です。
1本目で描いた地図のうち、新 API の Speech Summarization と Decisions を、実際にコードで叩いてパイプラインにします。

01.jpg

はじめに:読者の方からの、素敵な問いから始めます

1本目を公開したあと、こんな趣旨のコメントをいただきました。

商談の途中で条件が変わるケース——たとえば一度決めた日程や条件を、後から「やっぱりこうして」と訂正するような会話で、要約や判定は "訂正後" をちゃんと拾えるのか。そして 引用(根拠)も訂正後の発言 を指すのか。あとで会話を参照する用途を考えると、そこが気になる。

これは、AI で会議・商談を処理するときの 核心を突いた問い だと思いました。議事録や要約が「最初に言ったこと」に引きずられて、途中の訂正を取りこぼすと、下流の意思決定が丸ごと狂います。

そこで本記事では、次のパイプラインを実装し、実データで検証 します。

  • やること:商談音声を Speech Summarization で要約し、その結果を Decisions に渡して「受注見込み(スコア)」「次のアクション(選択)」などを 型付きで構造化判定 する
  • 検証する問い:
    1. 会話の途中で条件が変わっても、判定は 訂正後の値 を選べるか
    2. 引用 は訂正後の発言(=その時刻)を指すか
    3. 日本語の商談音声で、一連の処理が通るか(1本目で予告した事前検証)

素材は 架空の商談音声を2本 用意しました。1本は「値の訂正(日程・契約条件が後から変わる)」、もう1本は毛色を変えて「前提の訂正(判断の前提が途中でひっくり返る)」です。

録音内容はすべて架空です。Claude で架空の商談スクリプトを作り、VoiceStudio で T2S を行ったものをffmpeg でそれぞれ Customer/SalesRep のみの音声に分割し、さらに Zoom Meeting で実際に会議に流し込んで録音したものをベースにしています。

事前検証タスクの結果報告(1本目の宿題)

1本目で「日本語音声で通るか先に確認する」と予告していたので、先に結果だけ。

  • 日本語対応:合格 ✅ — 文字起こし・要約・Decisions のすべてを ja-JP で通して、問題なく動きました。要約は日本語の Markdown で、引用マーカー [^1] 付きで返ってきます。
  • モデル:zoom-decisions(標準)を使用 — Decisions には zoom-decisions と zoom-decisions-pro がありますが、zoom-decisions-pro は執筆時点では準備中 だったため、本記事は標準モデルで通しています。結論から言うと、商談の判定用途では 標準モデルで十分な精度 が出ました。

パイプライン全体像

今回のキモは、Decisions を2経路で流して比較する ところです。

なぜ2経路かというと、冒頭のコメントにあった 「要約すると情報が落ちるのでは?」 を直接検証したいからです。

  • 経路A(要約 → Decisions):構造化された要約を入力にする。情報は圧縮されるが、ノイズも減る。
  • 経路B(文字起こし → Decisions):生の全文を入力にする。情報は最大だが、ノイズも最大。

同じ質問を両方にぶつけて、答えが一致するか・どこで割れるか を見ます。これが後半の考察で効いてきます。

実装① Speech Summarization(音声 → 文字起こし → 要約)

エンドポイントは POST /v2/aiservices/speech-summarization/summarize。multipart/form-data で config(JSON)と file(音声)を送り、レスポンスは SSE(Server-Sent Events) で返ります。

依存ゼロ(Node 20+ の fetch / FormData / Blob だけ)で書けます。

// config は file より前に append するのがポイント
const config = {
  transcribe: { language: 'ja-JP', diarization: true, profanity_filter: false },
  summary: {
    language: 'ja-JP',
    citations: true,            // 引用(根拠)を付ける
    template_id: 'customer_success', // 商談向けテンプレート
  },
};

const form = new FormData();
form.append('config', JSON.stringify(config));                 // ← 先に config
form.append('file', new Blob([buf], { type }), 'demo-call.m4a'); // ← 次に file

const res = await fetch(
  'https://api.zoom.us/v2/aiservices/speech-summarization/summarize',
  {
    method: 'POST',
    headers: { Authorization: `Bearer ${token}`, Accept: 'text/event-stream' },
    body: form,
  },
);

レスポンスは SSE なので、ストリームを逐次パースします。主なイベントは transcribe_complete(文字起こし確定)、summary_updated(要約の更新。summary_result は累積で届く)、done です。

for await (const { event, data } of parseSSE(res.body)) {
  if (event === 'transcribe_complete') {
    transcript = data.transcript.result.text_display;
  } else if (event === 'summary_updated') {
    summary = data.summary_result;   // 最新で上書き
    citations = data.citations;      // [{ marker, start_time, end_time, ... }]
  }
}

ここで返ってくる要約は、見出し・表・箇条書きで構造化された Markdown です。そして各所に [^1] のような 引用マーカー が付き、citations[] にはそれぞれの 開始・終了時刻 が入っています。この「引用に時刻が付く」のが、今回の検証の裏取りに効きます。

02.png

実装② Decisions(テキスト → 型付きの構造化判定)

エンドポイントは POST /v2/aiservices/decisions/decide(同期)。input に判定したいテキスト、questions[] に 型付きの質問 を並べます。型は3種類です。

  • predicate:Yes/No を 確率(0〜1) で返す
  • choice:選択肢から1つ選び、各選択肢の確率分布 も返す
  • score:レベルに対する 加重スコア+分布 を返す

商談用の質問セット(抜粋)はこんな形です。

{
  "questions": [
    {
      "type": "choice",
      "name": "sale_start_date",
      "instructions": "商談で最終的に合意された『セール初日』はいつか。会話の途中で訂正があった場合は、訂正後(最後に合意された)日付を選ぶこと。",
      "choices": [
        { "value": "nov_27", "description": "11月27日" },
        { "value": "nov_20", "description": "11月20日" },
        { "value": "other_or_undecided", "description": "それ以外、または未定" }
      ]
    },
    {
      "type": "predicate",
      "name": "start_date_was_revised",
      "instructions": "会話の途中で、セール初日の日付が一度述べられた後に、別の日付へ変更・訂正されたか。"
    }
  ]
}

ポイントは instructions で 「訂正があったら訂正後を選べ」と明示 していること。ここの書き方が精度を大きく左右します(後述)。

呼び出し側はシンプルです。

const res = await fetch('https://api.zoom.us/v2/aiservices/decisions/decide', {
  method: 'POST',
  headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ input, questions, model: 'zoom-decisions' }),
});
const { answers } = await res.json();

レスポンスの answers[] に、質問ごとの判定が型に応じて入ります。

{
  "answers": [
    { "type": "choice", "name": "sale_start_date", "choice": "nov_20",
      "probabilities": [
        { "value": "nov_27", "probability": 0.0004 },
        { "value": "nov_20", "probability": 0.9994 }
      ] },
    { "type": "predicate", "name": "start_date_was_revised", "probability": 0.997 }
  ]
}

検証A:「値の訂正」は拾えるか(商談①)

1本目の音声は、途中で条件が2つ訂正される商談です。

  • セール初日:「11月27日」と言った後 → 「誤りで、正しくは1週間前倒しの 11月20日」に訂正
  • 契約:「1年契約・年額1,300万円」案 → 最終的に「2年契約・年額1,200万円」で合意(1年案は取り消し)

これを要約し、Decisions にかけた結果がこちらです(要約経由/文字起こし経由)。

質問 経路A(要約) 経路B(文字起こし) 正解
sale_start_date nov_20 nov_20 ✅ 訂正後
contract_term two_year_1200 two_year_1200 ✅ 訂正後
start_date_was_revised 0.997 0.998 ✅ 訂正を検知
next_action send_quote send_quote ✅

訂正後の値(11/20・2年契約)を、要約経由でも文字起こし経由でも、両方とも正しく拾えました。 「訂正があったか?」も 0.99 超で検知しています。

引用の裏取り

要約の「11月20日に前倒し」の記述には引用マーカー [^14] が付いていて、その citations を見ると——

[^14]  00:06:16 → 00:06:58

会話で 実際に日付を訂正したまさにその区間 を指していました。つまり「訂正後の結論」だけでなく「どこでそう決まったか」まで時刻で辿れます。冒頭のコメントの懸念(引用が訂正後を指すか)への答えは Yes です。

スクリーンショット 2026-11-03 9.46.44.png

検証B:「前提の訂正」は拾えるか(商談②)

2本目は、毛色を変えて 「値」ではなく「前提」がひっくり返る 商談にしました。

  • 当初は「暫定構成(月払い・月額58万円)なら担当者決裁で通る」と見ていた
  • ところが会話の途中で、経理の 決済基準が「支払総額」ではなく「年額換算」だと訂正 される → 年換算696万円で社長決裁対象に
  • 結果、暫定構成は見送り、4月の本契約(年額1,700万円)に一本化する方針へ転換
  • あわせて、見積の有効期限は 3月末まで延長 で合意

商談①用とは別に、この会話に合わせた質問セット(provisional_setup_decision ほか)を用意して判定しました。

質問 経路A(要約) 経路B(文字起こし) 正解
provisional_setup_decision skip(見送り) skip(見送り) ✅ 方針転換後
provisional_decision_was_reversed 0.979 0.997 ✅ 撤回を検知
approval_basis_was_corrected 0.999 0.9996 ✅ 前提の訂正を検知
estimate_valid_until end_of_march end_of_march ✅ 延長後

「値の訂正」だけでなく、「前提が訂正されて方針ごと覆る」ケースまで、両経路で正しく拾えました。 特に approval_basis_was_corrected(決済基準の訂正)を 0.999 で検知できたのは、議事録用途として心強い結果です。

おまけ:無い情報は「無い」と答えるか(ハルシネーション耐性)

試しに、商談①用の質問(sale_start_date など)を、まったく別内容の商談②にそのままぶつけてみました。結果は——

質問 商談②に当てた結果
sale_start_date other_or_undecided(該当なし)
contract_term undecided(該当なし)
start_date_was_revised 0.03(低い)

質問が内容と噛み合わないときは、無理に答えをでっち上げず「該当なし/低確率」と返してくれました。 型付き質問を使い回す運用でも、誤検知で暴走しにくいということです。

検証C:ヒント(priming)を外しても拾えるか

ここまでの検証A・Bには、実は弱点があります。質問文に 「訂正があれば訂正後を選べ」と書いていた こと。これだと「訂正を拾えた」のか「訂正を探せと指示したから探しただけ」なのか区別がつきません。

そこで、ヒントを一切外した中立版 を用意しました。

  • instructions から訂正に関する文言を全削除し、ただ 「合意された金額は?」「開始日は?(未定なら未定)」 と素朴に聞くだけにする
  • 両商談で 共通の中立スキーマ(金額/契約期間/主要日付/プロビジョニング判断/提供Tier/確度/次アクション)に統一する
  • 選択肢に 訂正前の値(11月27日・年額1,300万円)もあえて混在 させ、ヒントなしで後者=訂正後を選べるかを見る

結果:ヒントなしでも、訂正後を選んだ

商談①(値の訂正)・中立版

質問 経路A(要約) 経路B(文字起こし) 評価
contract_amount y1200 y1200 ✅ 1,300万に釣られず訂正後
contract_term two_year two_year ✅ 訂正後
key_scheduled_date nov_20 nov_20 ✅ 11/27が選択肢にあっても訂正後
next_action send_quote send_quote ✅

商談②(前提の訂正)・中立版

質問 経路A(要約) 経路B(文字起こし) 評価
contract_amount y1700 y1700 ✅
key_scheduled_date apr_1 apr_1 ✅
provisioning_decided 0.999 0.997 ✅
provisioning_tier vpc_dedicated vpc_dedicated ✅ 暫定見送り後

ヒントを一切与えず、しかも訂正前の値を選択肢に混ぜても、両経路とも訂正後の値(11/20・2年契約・1,200万円)を選びました。 つまり priming は精度の "底上げ" ではあっても "種明かし" ではなく、Decisions は会話の最新状態を地力で追えている と言えます。

ただし中立版では、新しい割れも見えました。これは次の考察でまとめて扱います。

考察:要約 vs 文字起こし、どっちに通すべきか

2経路を比較して、はっきりした傾向が出ました。

事実の抽出は、どちらでも頑健(むしろ要約が有利な場面も)

日付・契約・方針・「訂正されたか」といった 事実ベースの質問は、要約経由でも文字起こし経由でも答えが一致 しました。

それどころか、初期検証で質問が曖昧だったとき、文字起こし経由だけが会話に紛れた無関係な語(例:別件の「来週月曜」)に引っ張られて誤答 し、要約経由は構造化のおかげで正しく弾いた という場面もありました。要約は「情報を落とす」だけでなく、ノイズ除去としても効く わけです。

明示的に言い切られた事実は、instructions に「訂正があれば訂正後を」と書かなくても両経路で拾えました(検証C)。ヒントの明示は精度の "底上げ" にはなりますが、必須ではありません。とはいえ、質問文と選択肢の設計 が精度を左右するのは変わらないので、ここは丁寧に書く価値があります。

もう一つの割れ:言い切られていない事実は、要約が落とすことがある

中立版(検証C)で、新しいタイプの割れが見えました。会話の中で1文で言い切られず、複数の発言に分散している事実 です。

質問 経路A(要約) 経路B(文字起こし) 実態
商談① provisioning_tier undecided shared 「共有環境で行く」は確定だが、会話に分散
商談② contract_term undecided one_year 契約期間は明言されていない
  • 商談①では、「共有環境を採用する」という結論が会話のあちこちに散らばっていて、1文で言い切られていません。要約はこれを圧縮の過程で落とし、"未定" と答えました。一方で文字起こしは shared(正解)を拾えました。
  • 商談②では逆に、契約期間そのものが会話で明言されていません。ここでは 要約の "未定" の方が正直 で、文字起こしは「1年目の金額」という表現から one_year と 過剰に推論 しています。

➡️ 整理すると、事実抽出の傾向は次の非対称性で捉えられます。

  • 明示・訂正済みの事実(金額・日付など、はっきり言い切られる)→ 両経路で頑健。要約はノイズ除去としても効く。
  • 分散・暗黙の事実(Tier・契約期間など、言い切られない)→ 要約は落として "未定" になりやすい(安全側だが取りこぼし)、文字起こしは拾えるが過剰推論のリスク。

分散・暗黙の項目に、どう向き合うか(個人的意見)

「取りこぼし(recall)」と「誤検知(precision)」のトレードオフ 、正解は用途次第だと思います。

  1. 安全側に倒す(precision 重視) — 要約経由を使い、言い切られていない項目は "未定" のまま受け取る。判定をそのまま自動処理に流すなど、間違った断定を避けたい 用途向き。取りこぼしは後工程の人手レビューで拾う前提にします。

  2. ワイルドに攻める(recall 重視) — 文字起こし経由を使い、多少の過剰推論を許容してでも 拾いにいく。ドラフトの下書き・候補出しなど、人が最後に必ず確認する 用途向き。

  3. (推奨)項目を分解して、具体的な instructions を書く — 「Tierはどれか」と一発で聞くのではなく、判断に効く 関連項目を複数に分けて 抽出します。たとえば provisioning_tier(choice)に加えて、dedicated_env_required(predicate:専用環境が必須か)、shared_env_agreed(predicate:共有環境で合意したか)のように分解し、選択肢の description と instructions に 「どんな発言があればどれを選ぶか」の基準 まで書き込みます。こうすると要約・文字起こしのどちらでも精度が上がり、"未定" の取りこぼしも過剰推論も同時に減らせる=トレードオフ自体を小さくできます。手間は増えますが、本番運用で効かせたい項目にはこれが一番効きます。

スコア(主観判断)は割れることがある。そして、その"差"が情報になる

一方で、deal_likelihood(受注見込みスコア、0〜2)だけは挙動が分かれました。しかも、ヒントあり版(A/B)とヒントなし版(C)の 両方で同じ傾向が再現 しています。

経路A(要約) 経路B(文字起こし) 差
商談①(ヒントあり) 1.99 1.05 0.94(大きく割れた)
商談①(ヒントなし) 1.98 1.06 0.92(大きく割れた)
商談②(ヒントあり) 1.99 1.99 ≒0(一致)
商談②(ヒントなし) 1.98 1.99 ≒0(一致)

最初は「スコアは入力依存で割れるもの」と考えましたが、2商談 × 2条件で比べて解釈が変わりました。

  • 商談① は、解約条項が保留・決裁者の承認待ちなど、未解決の含みが会話に残って いました。文字起こしはその空気まで読んで慎重(1.05)に、要約はそれを均して楽観的(1.99)に振れた。
  • 商談② は、基準の訂正も方針も有効期限も その場で決着 していた。だから両方とも高スコアで一致した。

つまり、「要約スコア」と「文字起こしスコア」の差分そのものが、"要約が覆い隠した未解決リスクの量" を示すシグナル になり得ます。

まとめ

  • 明示された事実(日付・金額・決定事項・訂正の有無)→ 要約経由でOK。安定して速く、ノイズにも強い。ヒントを書かなくても拾えます。
  • 分散・暗黙のカテゴリ値(Tier・契約期間など、会話で言い切られない項目)→ 用途に応じて 「安全側に倒す(要約)/ワイルドに攻める(文字起こし)/項目を分解して具体化する(推奨)」 から選ぶ(前述の3択)。要約だけだと "未定" に落ちることがあります。
  • 主観スコア(見込み・リスク・温度感)→ 両方に通して差を見る。差が大きいほど「まだ固まっていない論点」が残っているサイン。

ハマりどころメモ

  • 要約テーブル内の改行が HTML エスケープされて返る — 表のセル内改行が <br> の形で返ってきます。そのまま Markdown 表示すると文字列として見えてしまうので、表示前にアンエスケープ するか、レンダラ側で処理してください。
  • 言語は明示指定が安心 — transcribe.language と summary.language をそれぞれ ja-JP で指定しました。文字起こしと要約で別言語にもできます。
  • SSE は逐次パース、要約は累積 — summary_updated の summary_result は差分ではなく累積で届くので、最新で上書きすればOKです。
  • (必要なら)一時エラーに備えてリトライを — 今回レート制限(429)等には遭遇しませんでしたが、SSE+同期呼び出しを本番運用するなら、一時的な失敗に備えて軽いリトライを入れておくと安心です。

まとめ

  • Speech Summarization(SSE・引用付き)と Decisions(型付き判定)を繋ぐと、商談音声 → 構造化された判定 までを少ないコードでパイプライン化可能
  • 会話の途中で条件が変わっても、「値の訂正」「前提の訂正」のどちらも、要約経由・文字起こし経由の両方で訂正後を正しく拾えた
  • しかも ヒント(priming)を外した中立版でも訂正後を拾えた(検証C)ので、これは指示頼みではなく Decisions の地力
  • 明示された事実は要約経由で頑健(ノイズ除去にもなる)
  • ただし 分散・暗黙のカテゴリ値は要約が "未定" に落とすことがある
  • 主観スコアは入力で割れることがある
  • 精度は「入力の選び方」と 「質問・選択肢の書き方」 の両輪

検証環境(執筆時点)

  • 日付:2026-11 時点のドキュメント/API 挙動
  • 実行:Node.js 20+(依存ゼロ、fetch / FormData / SSE 手動パース)
  • モデル:Speech Summarization(ja-JP)/ Decisions zoom-decisions
  • 素材:自作の架空商談音声2本

参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?