Azure Text to Speech で pt-PT ニューラル音声の audioOffset が 0 になる原因と対処法

Azure Text-to-Speech でポルトガル語(pt-PT)のニューラル音声を使うと、WordBoundary やブックマークの audioOffset が常に 0 になり、テキストハイライトとの同期が取れない――この記事では、この現象の背景と、実務で使える切り分け・回避策をまとめて解説します。

目次

Azure Text-to-Speech で pt-PT ニューラル音声の audioOffset が 0 になる問題とは

Azure Cognitive Services Speech(Text-to-Speech)では、synthesisWordBoundary やブックマーク、viseme などのイベントを購読することで、生成された音声の「いつどこを話しているか」をタイムスタンプとして取得できます。このタイムスタンプを利用すると、テキストの単語ハイライトやカラオケ風のアニメーションが簡単に実現できます。

ところが、以下のような構成で pt-PT のニューラル音声を使うと、synthesisWordBoundary およびブックマークの audioOffset が常に 0 のまま変化しない事象が報告されています。

  • 音声: pt-PT-RaquelNeural / pt-PT-DuarteNeural / pt-PT-FernandaNeural など
  • SDK: microsoft-cognitiveservices-speech-sdk(Node.js)
  • リージョン: North Europe(他リージョンでも再現する可能性あり)
  • 出力形式: Riff16Khz16BitMonoPcm
  • プロパティ: SpeechServiceResponse_SynthesisEventsSyncToAudio = "true" を明示的に指定

WordBoundary イベント自体は発火しているのに、audioOffset がずっと 0 のため、音声のどのタイミングでどの単語を話しているかが分からず、ハイライト同期が実現できません。

2025 年時点の Microsoft Q&A でも同様の質問が投稿されており、公式回答では「設定や SDK 側ではなく、pt-PT ニューラル音声モデル(あるいはそのバックエンド)のサービス側の問題・制限である可能性が高い」と分析されています。

つまり、アプリケーション側のコードをいくら調整しても、現状では pt-PT ニューラル音声からは「正しい単語タイムスタンプ」が得られない状況と考えるのが妥当です。

前提知識:WordBoundary / Viseme / Bookmark と audioOffset の仕組み

問題を切り分ける前に、Azure Speech SDK のメタデータイベントと audioOffset の意味を整理しておきます。

主要イベントと用途

イベント種別用途代表的なプロパティ
synthesisWordBoundary単語・文・句などの境界タイミングを通知。テキストハイライトや字幕同期に利用。audioOffset(音声内オフセット)、boundaryType、textOffset、wordLength など
Bookmark イベントSSML の <bookmark> タグ位置を通知。任意地点にマーカーを差し込める。audioOffset、ブックマーク名など
synthesisViseme口形(viseme)ごとのタイミングを通知。リップシンクやアバター表示で利用。audioOffset、visemeId など

audioOffset の単位と換算

Speech SDK の WordBoundary / Viseme / Bookmark イベントに含まれる audioOffset は、いずれも tick(100 ナノ秒)単位です。

  • 1 tick = 100 ns = 0.0001 ms
  • 1 ms = 10,000 tick

そのため、JavaScript / TypeScript では次のようにミリ秒へ換算します。

const ticksToMs = (ticks: number): number =&gt; ticks / 10_000;

単位を誤解して「とても小さな値だから 0 だ」と見なしてしまうケースもありますが、今回の pt-PT の問題は 文字通り数値が常に 0 である点が特徴です。

SpeechServiceResponse_SynthesisEventsSyncToAudio の役割

プロパティ SpeechServiceResponse_SynthesisEventsSyncToAudio は、「WordBoundary や Viseme などのメタデータイベントを、SDK が再生している音声に同期して発火するかどうか」を制御するフラグです。

  • true(既定値):
    • イベントは音声の再生タイミングに合わせて発火します。
    • audioOffset 自体は 0 から増加する tick 値のままですが、イベントの「届くタイミング」が再生に揃えられます。
  • false:
    • サービスから届いた順にイベントが飛んできます。
    • ストリーミング再生中に、再生位置よりかなり先のイベントが早めに届くことがあります。

今回の pt-PT 問題は「イベント到着タイミング」ではなく audioOffset 値そのものが 0 固定 という症状なので、このプロパティの設定に起因するものとは考えにくい、というのがポイントです。

なぜ pt-PT ニューラル音声で audioOffset が 0 になるのか

Microsoft Q&A の公式回答と、他言語での動作報告を踏まえると、次のように整理できます。

  • Node.js Speech SDK の設定や、音声形式(PCM / RIFF)自体は問題ない。
  • synthesisWordBoundary イベントは正しく発火しているため、「WordBoundary を要求する設定」は通っている。
  • それにもかかわらず audioOffset が常に 0 ということは、「pt-PT ニューラル音声モデル(あるいはそのバックエンド)」が、WordBoundary のタイミング情報を生成していないか、生成しても 0 のまま返してしまっている。

つまり、現時点では pt-PT ニューラル音声固有のサービス側不具合または未対応機能 と考えるのが自然です。

実際、過去の Speech SDK リリースノートでは JavaScript 版の audioOffset 計算バグが修正されたこともあり、SDK 側の実装バグが audioOffset に影響するケースが存在する ことが分かっています。 しかし、同じアプリケーションで他言語のニューラル音声を使うと audioOffset が正常に増加するのであれば、pt-PT だけが例外的な挙動をしていると判断できます。

短時間でできる切り分けチェックリスト

「自分の環境固有の問題」なのか「サービス側の問題」なのかを見極めるために、次のチェックをおすすめします。いずれも数分で試せます。

チェック項目具体的な操作結果から分かること
他言語の代表的音声に変更en-US-JennyNeural などに差し替えて同じコードを実行他言語で audioOffset が 0 以外で単調増加するなら、pt-PT 音声モデル固有の問題
リージョン変更North Europe 以外(West Europe / East US など)で同じコードを実行特定リージョンのデプロイにだけ問題がある可能性を切り分けできる
Viseme イベントの確認synthesisViseme を購読し、audioOffset をログ出力Viseme の audioOffset が正しく増加するなら、「WordBoundary 生成部分」だけに問題がある可能性が高い
SDK バージョンを最新化microsoft-cognitiveservices-speech-sdk を最新版に更新古い JS SDK の audioOffset 計算バグを踏んでいないか確認できる
WordBoundary 要求フラグの明示SpeechServiceResponse_RequestWordBoundary を "true" に設定WordBoundary イベントが来ない場合の典型的な設定漏れを除外できる

これらを試しても「pt-PT ニューラル音声だけ audioOffset が 0 固定」のままであれば、ほぼ確実にサービス側の問題です。アプリ側でできることは限られるので、次のような回避策とサポート依頼に進みましょう。

Node.js Speech SDK での実装例(正常系確認用)

まずは、他言語では正しく audioOffset が取れることを確認するための、シンプルなサンプルコードを載せておきます。

const sdk = require("microsoft-cognitiveservices-speech-sdk");

const speechConfig = sdk.SpeechConfig.fromSubscription(
  process.env.AZURE_SPEECH_KEY,
  process.env.AZURE_SPEECH_REGION
);

// ★ここを切り替えて挙動を比較
// speechConfig.speechSynthesisVoiceName = "pt-PT-RaquelNeural";
speechConfig.speechSynthesisVoiceName = "en-US-JennyNeural";

// WordBoundary イベントを明示的に要求(保険)
speechConfig.setProperty(
  sdk.PropertyId.SpeechServiceResponse_RequestWordBoundary,
  "true"
);

// イベントを音声再生に同期
speechConfig.setProperty(
  sdk.PropertyId.SpeechServiceResponse_SynthesisEventsSyncToAudio,
  "true"
);

const synthesizer = new sdk.SpeechSynthesizer(speechConfig);

const ticksToMs = (ticks) =&gt; Number(ticks) / 10_000;

// 単語境界
synthesizer.synthesisWordBoundary = (_s, e) =&gt; {
  const ms = ticksToMs(e.audioOffset);
  console.log(
    `[WordBoundary] ${ms.toFixed(1)} ms, text="${e.text}", boundary=${e.boundaryType}`
  );
};

// Viseme
synthesizer.synthesisViseme = (_s, e) =&gt; {
  const ms = ticksToMs(e.audioOffset);
  console.log(`[Viseme] id=${e.visemeId} at ${ms.toFixed(1)} ms`);
};

const ssml = `
&lt;speak version="1.0" xml:lang="en-US"&gt;
  &lt;voice name="en-US-JennyNeural"&gt;
    Hello! This is a timing test.
  &lt;/voice&gt;
&lt;/speak&gt;
`;

synthesizer.speakSsmlAsync(
  ssml,
  (result) =&gt; {
    console.log("Synthesis completed.", result.reason);
    synthesizer.close();
  },
  (error) =&gt; {
    console.error("Synthesis error:", error);
    synthesizer.close();
  }
);

上記コードで en-US-JennyNeural を使った場合、ログの ms が単調増加していれば SDK 側の処理は正常と判断できます。そのうえで、同じコードの音声名だけを pt-PT-RaquelNeural に変え、audioOffset が 0 固定になるかどうかを確認すると、問題の切り分けがはっきりします。

暫定回避策:本番運用を止めないためのアプローチ

サービス側の修正を待つ間、アプリを止めずに運用するための現実的な回避策をいくつか紹介します。それぞれメリットとトレードオフがあるので、要件に応じて組み合わせてください。

代替音声モデルを採用する

最も単純かつ確実なのは、WordBoundary が正しく動作する別の音声モデルを使うことです。

  • ポルトガル語であれば、ブラジル向けの pt-BR-* ニューラル音声を試す。
  • 学習アプリなどで「音声は英語でもよい」場合、en-US-JennyNeural のような実績の多い英語音声を使う。
  • UI だけ pt-PT で、音声は暫定的に pt-BR or en-US にする、といった妥協案もあり。

Azure Speech の言語サポート一覧では、各音声のロケールとステータス(GA / Preview / Deprecated)が確認できます。対象音声が Preview や Deprecated の場合、こうした不具合が出やすい傾向があるため、可能であれば GA(一般提供)の音声を優先するのがおすすめです。

Viseme ベースで概算タイミングを割り当てる

pt-PT でも synthesisViseme の audioOffset が正しく増加している場合は、Viseme イベントを擬似的な「タイムライン」として利用する方法があります。

簡略化したアルゴリズムのイメージは次のとおりです。

  1. テキストを単語単位に分割する(空白や句読点で split)。
  2. Viseme イベントの audioOffset をすべて配列に格納し、音声全体の長さの近似値とみなす。
  3. 単語数で Viseme オフセットを均等割り、もしくは「各単語の文字数」に応じて重みづけして割り当てる。
  4. 割り当てたオフセットをもとに、フロントエンドでハイライトを進める。

あくまで近似なので、厳密な単語境界ではありませんが、eラーニングや読み上げ UI では「多少ズレても視覚的に違和感が少ない」ことが多く、暫定対応としては十分実用的です。

const visemes = [];

synthesizer.synthesisViseme = (_s, e) =&gt; {
  visemes.push(ticksToMs(e.audioOffset));
};

// 合成完了後に単語ごとの概算タイミングを計算する例
function calcApproxWordTimings(text, visemeOffsetsMs) {
  const words = text.trim().split(/\s+/);
  if (visemeOffsetsMs.length === 0 || words.length === 0) {
    return words.map((w, i) =&gt; ({ word: w, timeMs: i * 300 }));
  }

  const totalDuration = visemeOffsetsMs[visemeOffsetsMs.length - 1];
  const totalChars = words.reduce((sum, w) =&gt; sum + w.length, 0);

  let accChars = 0;
  return words.map((word) =&gt; {
    const ratio = accChars / totalChars;
    const timeMs = totalDuration * ratio;
    accChars += word.length;
    return { word, timeMs };
  });
}

このように「Viseme の合計長 ≒ 音声全体の再生時間」と仮定して、単語ごとに時間を割り当てることで、WordBoundary が壊れていても最低限の同期を実現できます。

文・句単位に分割して別々に合成する

単語単位のハイライトが必須ではなく、「文ごと・フレーズごとに色を変える」程度でよい場合は、テキストを文単位で分割し、それぞれを別々に TTS する方法も有効です。

  • 1 文ごとに SSML ドキュメントを生成して合成。
  • 各文の再生時間は、合成後の音声長(ファイルの長さ)を計測して取得。
  • フロントエンドでは「文の再生命令が走ったら、その文をハイライト」といった単純なロジックにする。

この方法であれば、WordBoundary や audioOffset に頼らずに「文レベルの同期」が実現できます。文章量が多くても、バッチ合成 API を使えば並列で合成を進められるため、教材コンテンツなどにも向いています。

外部の強制アライメントツールを使って精密タイムスタンプを取得する

「どうしても pt-PT の音声で、単語レベルの正確なタイムスタンプが必要だ」という場合は、Azure TTS で一度音声を生成したあと、オープンソースの 強制アライナー(forced aligner) に音声とテキストを渡して、単語タイミングを推定するという方法があります。

代表的な強制アライナーには次のようなものがあります。

  • Montreal Forced Aligner (MFA):
    • Kaldi ベースの高精度アライナー。
    • 単語・音素レベルのアラインメントをサポート。
    • 複数言語に対応可能で、カスタムモデルもトレーニング可能。
  • Gentle:
    • こちらも Kaldi ベースで、英語向けに実績の多いアライナー。
    • 比較的セットアップが容易で、小規模なプロジェクトに向く。

運用イメージは次のとおりです。

  1. Azure TTS(pt-PT)で最終的に配信したい音声を生成し、WAV ファイルとして保存。
  2. 同じテキスト(または簡易的にクリーンアップしたテキスト)を強制アライナーに渡し、アラインメントを実行。
  3. アライナーから出力される TextGrid(Praat 形式)や JSON などを解析し、単語ごとの開始時刻・終了時刻を抽出。
  4. そのタイムスタンプをアプリケーションのハイライト同期に利用。

セットアップや処理時間はかかりますが、サービス側の修正を待たずに 高精度な単語タイミング を得られるため、コンテンツ制作パイプラインなどでは現実的な選択肢になります。

公式サポート・GitHub への報告で恒久対応を促す

サービス側のバグあるいは機能未対応である可能性が高い以上、最終的には Microsoft 側で修正してもらう必要があります。そのために、次の 2 つの窓口を活用するのがおすすめです。

Azure サポート(ポータル)でチケットを起票する

有償サポート契約がある場合は、Azure ポータルからサポートリクエストを作成できます。

  • 対象サービス: 「Azure AI Speech(Cognitive Services)」
  • 問題タイプ: Text-to-Speech > SDK > メタデータイベント など、できるだけ近いものを選択

チケットには、次の情報をできるだけ詳しく記載すると、調査がスムーズになります。

  • 利用している音声名(例: pt-PT-RaquelNeural ほか)
  • リージョン(例: North Europe)
  • Speech SDK のバージョン(例: 1.39.0 など)
  • Node.js のバージョン
  • 最小再現可能なソースコード(SSML と Node.js のコードを数十行程度に絞ったもの)
  • 実際のログ(一連の WordBoundary イベントで audioOffset が 0 のままであることが分かるもの)
  • 合成レスポンスに含まれる ServiceRequestId(取得できる場合)

サポートチーム側で再現・エスカレーションが行われると、将来のアップデートで修正される可能性が高まります。

GitHub Issues で開発チームに共有する

有償サポートがない場合でも、Azure-Samples の Speech SDK リポジトリでは Issues が受け付けられており、開発チームがモニタリングしています。

  • リポジトリ: Azure-Samples/cognitive-services-speech-sdk
  • Issue テンプレートに従い、OS / SDK バージョン / 再現コード / ログなどを添付

すでに同様の Issue が立っている場合は、「自分もこの問題に影響を受けている」 というコメントを追加し、環境情報や再現パターンを追記しておくと、優先度が上がりやすくなります。

よくある勘違いと確認ポイント

最後に、pt-PT 問題とは直接関係しないものの、WordBoundary / audioOffset 周りでよくハマるポイントをまとめておきます。

audioOffset の単位を ms だと思い込んでしまう

前述のとおり、audioOffset は tick(100ns)単位なので、そのまま ms として扱うと 10,000 倍ズレます。

  • 1234567 tick は、約 123.4567 ms
  • ミリ秒にしたい場合は、必ず / 10_000 する

WordBoundary をリクエストしていないためイベントが来ない

SDK のバージョンによっては、WordBoundary イベントを受け取るために SpeechServiceResponse_RequestWordBoundary を明示的に "true" にする必要があります。

イベント自体が 1 つも来ない場合は、まずこのフラグの有無を確認しましょう。今回の pt-PT 問題では「イベントは来ているのに audioOffset が 0」という状況なので別問題ですが、切り分けの初期段階としては必須のチェックです。

イベントの発火タイミングと audioOffset を混同してしまう

SpeechServiceResponse_SynthesisEventsSyncToAudio を false にしていると、イベントは再生よりも早く届くことがあります。しかし、audioOffset の数値自体は「音声ストリーム内での位置」を示すため、再生位置との同期は「イベントが来た瞬間」ではなく「audioOffset の値」を基準に行う必要があります。

Web プレイヤーなどと連携する場合は、「プレイヤーの現在再生位置(ms)が、イベントの audioOffset(ms換算)を超えたらハイライトする」といったロジックで同期を取ると、多少のイベント遅延があっても問題になりにくくなります。

今後のアップデートを追いかけるには

pt-PT ニューラル音声の WordBoundary 対応状況は、現時点では公式ドキュメントに明示されていませんが、Speech SDK のリリースノートや Speech Service のドキュメントを定期的に確認しておくと、問題修正や新機能追加をキャッチしやすくなります。

  • Speech SDK リリースノート:
    • 「audio offset」や「word boundary」などのキーワードで検索し、関連する修正が入っていないかを確認。
  • Speech Service の概要・チュートリアル:
    • Text-to-Speech の「イベント」や「SSML」の章に、WordBoundary / Bookmark / Viseme に関する更新がないかチェック。
  • Microsoft Q&A / GitHub Issues:
    • 同様の問題を抱える開発者からの最新コメントや、チームからの回答を確認。

特に「pt-PT のニューラル音声名」や「audioOffset 0」などで検索しておくと、同じ問題がどの程度の開発者に影響しているか、修正状況はどうかを把握しやすくなります。

まとめ:pt-PT ニューラル音声の WordBoundary は慎重に扱う

この記事のポイントを整理すると、次のようになります。

  • pt-PT のニューラル音声(pt-PT-RaquelNeural など)で synthesisWordBoundary / ブックマークの audioOffset が 0 固定になるのは、SDK 設定ミスではなく、音声モデル側(サービス側)の不具合・未対応 である可能性が高い。
  • 他言語のニューラル音声や他リージョンで audioOffset が正常に増加するかを確認し、「pt-PT 固有の問題」であることを切り分けるのが第一歩。
  • 恒久的な解決には、Azure サポートや GitHub Issues 経由での 公式へのフィードバック が不可欠。
  • 修正までの間は、次のような暫定回避策を組み合わせると実用的なワークアラウンドになる。
    • 別言語・別音声モデル(pt-BR, en-US など)への一時的な切り替え
    • Viseme イベントを利用した概算タイミングの割り当て
    • 文・句単位での分割合成による「粗い同期」
    • 外部強制アライナー(MFA / Gentle など)による高精度なオフラインアライメント
  • audioOffset は 100ns 単位の tick であり、ミリ秒に変換するには / 10_000 が必要であることを忘れない。

pt-PT 向けの学習アプリや読み上げサービスを設計する際は、「現状 WordBoundary が正常に出ない可能性がある」という前提で UI / データ構造を設計しておくと、将来的にサービス側が修正されたときもスムーズに移行できます。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次