最初のパケット遅延が短いテキスト読み上げ変換をストリーミングで実現します。リアルタイム音声合成は、ストリーミング入出力、音声クローニング、音声デザイン、および音声アシスタント、オーディオブック、インテリジェントカスタマーサービス向けの詳細なオーディオ制御をサポートしています。
概要
低遅延でテキストをリアルタイムに音声に変換します。
- 最初のパケット遅延が短いストリーミング入出力
- 発話速度、ピッチ、ボリューム、ビットレートを調整して、詳細なオーディオ制御を実現
- 主流のオーディオフォーマット(PCM、WAV、MP3、Opus)に対応し、最大 48 kHz のサンプルレートで出力可能
- 命令による制御をサポート。自然言語による命令で発話の表現力を制御可能
- 音声クローニングおよび音声デザインをサポートし、カスタム音声を作成可能
- 感情およびリッチ言語タグをサポート。テキスト内に感情や効果音タグを埋め込み可能
オーディオブックや教材の音声読み上げなどバッチ処理が必要なシナリオでは、非リアルタイム音声合成をご利用ください。モデル選択のガイダンスについては、「音声合成」をご参照ください。
前提条件
- API キーを設定し、環境変数として設定します。
- DashScope SDK 経由で API を呼び出す場合は、最新の SDK をインストールします。
- CosyVoice モデルで AOQ プロトコルを使用するには、AOQ クライアント SDK をダウンロードして統合します。詳細については、「SDK 概要」をご参照ください。
クイックスタート
以下の例は、各モデルの音声合成方法を示しています。その他の例およびパラメーターの詳細については、「API リファレンス」をご参照ください。
Qwen-Audio-TTS
以下の例は、システム音声を使用して音声合成を行います。
命令による制御機能を使用するには、instruction パラメーターを通じて命令を設定します。
# coding=utf-8
import os
import dashscope
from dashscope.audio.tts_v2 import *
# API キーはシンガポールリージョンと中国 (北京) リージョンで異なります。API キーの取得: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
# 環境変数を設定していない場合は、次の行を中国 Model Studio API キーに置き換えてください: dashscope.api_key = "sk-xxx"
dashscope.api_key = os.environ.get('DASHSCOPE_API_KEY')
# 以下はシンガポールリージョンの構成です。{WorkspaceId} を実際のワークスペース ID に置き換えてください。リージョンによって構成は異なります。
dashscope.base_websocket_api_url='wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'
# モデル
# qwen-audio-3.0-tts-flash/qwen-audio-3.0-tts-plus: longanhuan_v3.6 などの音声を使用します。
# 各音声は異なる言語をサポートしています。日本語や韓国語など中国語以外の言語を合成するには、対象言語をサポートする音声を選択してください。詳細は音声リストをご参照ください。
model = "qwen-audio-3.0-tts-flash"
# 音声
voice = "longanhuan_v3.6"
# SpeechSynthesizer をインスタンス化し、コンストラクターで model や voice などのリクエストパラメーターを渡します
synthesizer = SpeechSynthesizer(model=model, voice=voice)
# 合成するテキストを送信し、バイナリオーディオを取得します
audio = synthesizer.call("How is the weather today?")
# 最初のテキスト送信時には WebSocket 接続を確立する必要があるため、最初のパケット遅延には接続設定時間が含まれます
print('[Metric] requestId: {}, first-packet latency: {} ms'.format(
synthesizer.get_last_request_id(),
synthesizer.get_first_package_delay()))
# オーディオをローカルファイルに保存します
with open('output.mp3', 'wb') as f:
f.write(audio)
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesisParam;
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesizer;
import com.alibaba.dashscope.utils.Constants;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
public class Main {
// モデル
// qwen-audio-3.0-tts-flash/qwen-audio-3.0-tts-plus: longanhuan_v3.6 などの音声を使用します。
// 各音声は異なる言語をサポートしています。日本語や韓国語など中国語以外の言語を合成するには、対象言語をサポートする音声を選択してください。詳細は音声リストをご参照ください。
private static String model = "qwen-audio-3.0-tts-flash";
// 音声
private static String voice = "longanhuan_v3.6";
public static void streamAudioDataToSpeaker() {
// リクエストパラメーター
SpeechSynthesisParam param =
SpeechSynthesisParam.builder()
// API キーはシンガポールリージョンと中国 (北京) リージョンで異なります。API キーの取得: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// 環境変数を設定していない場合は、次の行を中国 Model Studio API キーに置き換えてください: .apiKey("sk-xxx")
.apiKey(System.getenv("DASHSCOPE_API_KEY"))
.model(model) // モデル
.voice(voice) // 音声
.build();
// 同期モード: コールバックを無効化(2 番目のパラメーターは null)
SpeechSynthesizer synthesizer = new SpeechSynthesizer(param, null);
ByteBuffer audio = null;
try {
// オーディオが返されるまでブロックします
audio = synthesizer.call("How is the weather today?");
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// タスク完了時に WebSocket 接続を閉じます
synthesizer.getDuplexApi().close(1000, "bye");
}
if (audio != null) {
// オーディオデータをローカルファイル "output.mp3" に保存します
File file = new File("output.mp3");
// 最初のテキスト送信時には WebSocket 接続を確立する必要があるため、最初のパケット遅延には接続設定時間が含まれます
// 注: getFirstPackageDelay() には dashscope-sdk-java 2.18.0 以降が必要です
System.out.println(
"[Metric] requestId: "
+ synthesizer.getLastRequestId()
+ ", first-packet latency (ms): "
+ synthesizer.getFirstPackageDelay());
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(audio.array());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public static void main(String[] args) {
// 以下はシンガポールリージョンの構成です。{WorkspaceId} を実際のワークスペース ID に置き換えてください。リージョンによって構成は異なります。
Constants.baseWebsocketApiUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference";
streamAudioDataToSpeaker();
System.exit(0);
}
}
CosyVoice
このモデルは WebSocket に加え、AOQ プロトコルもサポートしています。安定した遅延、弱いネットワークでの回復力、および内蔵の全二重ノイズ抑制とエコーキャンセルを優先するクライアント側統合には、AOQ を推奨します。プロトコルの比較については、「リアルタイム API 概要」をご参照ください。
以下の例は、システム音声を使用して音声合成を行います(CosyVoice 音声リストをご参照ください)。
命令による制御機能を使用するには、instruction パラメーターを通じて命令を設定します。
# coding=utf-8
import os
import dashscope
from dashscope.audio.tts_v2 import *
# API キーはシンガポールリージョンと中国 (北京) リージョンで異なります。API キーの取得: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
# 環境変数を設定していない場合は、次の行を中国 Model Studio API キーに置き換えてください: dashscope.api_key = "sk-xxx"
dashscope.api_key = os.environ.get('DASHSCOPE_API_KEY')
# 以下はシンガポールリージョンの構成です。{WorkspaceId} を実際のワークスペース ID に置き換えてください。リージョンによって構成は異なります。
dashscope.base_websocket_api_url='wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'
# モデル
# モデルのバージョンによって対応する音声が異なります:
# cosyvoice-v3-flash/cosyvoice-v3-plus: longanyang などの音声を使用します。
# cosyvoice-v2: longxiaochun_v2 などの音声を使用します。
# 各音声は異なる言語をサポートしています。日本語や韓国語など中国語以外の言語を合成するには、対象言語をサポートする音声を選択してください。詳細は Qwen-Audio-TTS/CosyVoice 音声リストをご参照ください。
model = "cosyvoice-v3-flash"
# 音声
voice = "longanyang"
# SpeechSynthesizer をインスタンス化し、コンストラクターで model や voice などのリクエストパラメーターを渡します
synthesizer = SpeechSynthesizer(model=model, voice=voice)
# 合成するテキストを送信し、バイナリオーディオを取得します
audio = synthesizer.call("How is the weather today?")
# 最初のテキスト送信時には WebSocket 接続を確立する必要があるため、最初のパケット遅延には接続設定時間が含まれます
print('[Metric] requestId: {}, first-packet latency: {} ms'.format(
synthesizer.get_last_request_id(),
synthesizer.get_first_package_delay()))
# オーディオをローカルファイルに保存します
with open('output.mp3', 'wb') as f:
f.write(audio)
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesisParam;
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesizer;
import com.alibaba.dashscope.utils.Constants;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
public class Main {
// モデル
// モデルのバージョンによって対応する音声が異なります:
// cosyvoice-v3-flash/cosyvoice-v3-plus: longanyang などの音声を使用します。
// cosyvoice-v2: longxiaochun_v2 などの音声を使用します。
// 各音声は異なる言語をサポートしています。日本語や韓国語など中国語以外の言語を合成するには、対象言語をサポートする音声を選択してください。詳細は Qwen-Audio-TTS/CosyVoice 音声リストをご参照ください。
private static String model = "cosyvoice-v3-flash";
// 音声
private static String voice = "longanyang";
public static void streamAudioDataToSpeaker() {
// リクエストパラメーター
SpeechSynthesisParam param =
SpeechSynthesisParam.builder()
// API キーはシンガポールリージョンと中国 (北京) リージョンで異なります。API キーの取得: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// 環境変数を設定していない場合は、次の行を中国 Model Studio API キーに置き換えてください: .apiKey("sk-xxx")
.apiKey(System.getenv("DASHSCOPE_API_KEY"))
.model(model) // モデル
.voice(voice) // 音声
.build();
// 同期モード: コールバックを無効化(2 番目のパラメーターは null)
SpeechSynthesizer synthesizer = new SpeechSynthesizer(param, null);
ByteBuffer audio = null;
try {
// オーディオが返されるまでブロックします
audio = synthesizer.call("How is the weather today?");
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// タスク完了時に WebSocket 接続を閉じます
synthesizer.getDuplexApi().close(1000, "bye");
}
if (audio != null) {
// オーディオデータをローカルファイル "output.mp3" に保存します
File file = new File("output.mp3");
// 最初のテキスト送信時には WebSocket 接続を確立する必要があるため、最初のパケット遅延には接続設定時間が含まれます
// 注: getFirstPackageDelay() には dashscope-sdk-java 2.18.0 以降が必要です
System.out.println(
"[Metric] requestId: "
+ synthesizer.getLastRequestId()
+ ", first-packet latency (ms): "
+ synthesizer.getFirstPackageDelay());
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(audio.array());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public static void main(String[] args) {
// 以下はシンガポールリージョンの構成です。{WorkspaceId} を実際のワークスペース ID に置き換えてください。リージョンによって構成は異なります。
Constants.baseWebsocketApiUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference";
streamAudioDataToSpeaker();
System.exit(0);
}
}
高度な機能
命令による制御
命令による制御は、複雑なオーディオパラメーターを設定することなく、自然言語による記述で発話のトーン、速度、感情、音色特性を調整します。
モデル別の命令仕様:
Qwen-Audio-TTS
サポートモデル: qwen-audio-3.0-tts-plus、qwen-audio-3.0-tts-flash
システム音声および音声クローニング音声: 任意の命令を受け付けます。
CosyVoice
サポートモデル: cosyvoice-v3.5-plus、cosyvoice-v3.5-flash、cosyvoice-v3-plus、cosyvoice-v3-flash
モデルによって命令フォーマットの要件が異なります:
-
cosyvoice-v3.5-plus、cosyvoice-v3.5-flash:
- 音声クローニング/デザイン音声: 任意の命令を受け付けます。
- システム音声: v3.5 はシステム音声をサポートしていません。
-
cosyvoice-v3-plus:
- 音声クローニング/デザイン音声: 命令による制御をサポートしていません。
- システム音声: 命令は固定フォーマットおよび内容を使用する必要があります。「CosyVoice 音声リスト」をご参照ください。
-
cosyvoice-v3-flash:
- 音声クローニング/デザイン音声: 任意の命令を受け付けます。
- システム音声: 命令は固定フォーマットおよび内容を使用する必要があります。「CosyVoice 音声リスト」をご参照ください。
使用方法: instruction パラメーターを通じて命令内容を指定します。
命令テキストのサポート言語:
-
cosyvoice-v3.5-plus、cosyvoice-v3.5-flash:
- 音声クローニング/デザイン音声: 中国語、英語、フランス語、ドイツ語、日本語、韓国語、ロシア語、ポルトガル語、タイ語、インドネシア語、ベトナム語。
- システム音声: v3.5 はシステム音声をサポートしていません。
-
cosyvoice-v3-plus:
- 音声クローニング/デザイン音声: 中国語、英語、フランス語、ドイツ語、日本語、韓国語、ロシア語。
- システム音声: 命令は固定フォーマットおよび内容を使用する必要があります。「CosyVoice 音声リスト」をご参照ください。
-
cosyvoice-v3-flash:
- 音声クローニング/デザイン音声: 中国語、英語、フランス語、ドイツ語、日本語、韓国語、ロシア語。
- システム音声: 中国語のみ。
命令テキストの文字数制限: 最大 100 文字。漢字(簡体字/繁体字、日本語の漢字、韓国の漢字)は 1 文字あたり 2 文字としてカウントされます。その他の文字(句読点、アルファベット、数字、日本語のかな、韓国のハングル)は 1 文字としてカウントされます。
ユースケース:
- オーディオブックおよびラジオドラマの音声読み上げ
- 広告およびプロモーションビデオの音声読み上げ
- ゲームキャラクターおよびアニメーションの音声読み上げ
- 感情表現豊かな音声アシスタント
- ドキュメンタリーおよびニュース放送のナレーション
効果的な音声記述の作成:
-
基本原則:
- 具体的で曖昧でない: 「深みのある」、「明瞭な」、「やや速いペース」など、音声の質を描写する言葉を使用します。主観的または曖昧な用語(例: 「良い」、「普通」)は避けてください。
- 多面的で一面的でない: 良い記述は通常、複数の次元(性別、年齢、感情など)をカバーします。「女性の声」とだけ書くと、特徴的な音色を生成するには不十分です。
- 客観的で主観的でない: 音声の物理的および知覚的特性に焦点を当てます。たとえば、「お気に入りの声」ではなく、「高いピッチでエネルギッシュなトーン」を使用します。
- 独創的で模倣的でない: 特定の個人(有名人や俳優など)の模倣を要求するのではなく、音声の質を記述します。モデルは模倣をサポートしておらず、著作権リスクを引き起こす可能性があります。
- 簡潔で冗長でない: すべての単語が目的を持つようにします。繰り返しの同義語や意味のない修飾語は避けてください。
-
記述次元の参考:
以下の次元を組み合わせて音声を記述します。次元を多く含めるほど、出力が正確になります。
次元 | 記述例 |
|---|---|
性別 | 男性、女性、中性的 |
年齢 | 子供 (5-12 歳)、ティーンエイジャー (13-18 歳)、若い成人 (19-35 歳)、中年 (36-55 歳)、高齢者 (55 歳以上) |
ピッチ | 高め、中間、低め、やや高め、やや低め |
速度 | 速め、普通、遅め、やや速め、やや遅め |
感情 | 明るい、落ち着いた、優しい、真剣な、活発な、冷静な、癒し系 |
特性 | 磁性的、明瞭な、かすれた、まろやかな、甘い、豊かな、力強い |
ユースケース | ニュース放送、広告音声、オーディオブック、アニメーションキャラクター、音声アシスタント、ドキュメンタリーナレーション |
-
例:
- 標準放送スタイル: 明瞭で正確な発音、完璧な発音
- 若くて活発な女性の声で、やや速いペースと顕著な上昇イントネーション。ファッション製品の紹介に適しています
- 落ち着いた中年の男性で、ゆっくりとしたペース、深みがあり磁性的な声。ニュース読み上げやドキュメンタリーナレーションに適しています
- 優しく知的な 30 歳前後の女性で、均一なトーン。オーディオブックのナレーションに適しています
- かわいらしい子供の声で、約 8 歳の女の子。やや幼い話し方。アニメーションキャラクターの音声読み上げに適しています
方言
このセクションでは、中国語方言(河南方言、四川方言、広東語など)で音声を生成する方法について説明します。モデルおよび音声タイプによって構成方法が異なります。
モデル別の方言構成:
Qwen-Audio-TTS
-
システム音声: 以下のいずれかの音声タイプを選択します:
- 組み込みの方言サポートを持つシステム音声。追加の構成なしで対応する方言が出力されます。
- 命令による制御をサポートする音声。命令テキストを通じて特定の方言を構成できます。
-
音声クローニング音声: 命令による制御機能を通じて構成します。たとえば、命令テキストを
请用河南话表达に設定します。
サポート方言: 「Qwen-Audio-TTS」の各モデルの「サポート言語」列をご参照ください。
CosyVoice
-
システム音声: 「CosyVoice 音声リスト」から以下のいずれかの音声タイプを選択します:
- 組み込みの方言サポートを持つシステム音声(例:
longshange_v3)。追加の構成なしで対応する方言が出力されます。 - 命令による制御をサポートする音声(例:
longanhuan_v3)。命令テキストを通じて特定の方言を指定できます。
- 組み込みの方言サポートを持つシステム音声(例:
-
音声クローニング音声: 命令による制御機能を通じて構成します。たとえば、命令テキストを
请用河南话表达に設定します。 -
音声デザイン音声: 方言をサポートしていません。
サポート方言: 「CosyVoice」の各モデルの「サポート言語」列をご参照ください。
例: cosyvoice-v3-flash モデルと longanhuan_v3 音声を使用し、命令テキストを "请用河南话表达。" に設定して、河南方言で音声を出力します。
# coding=utf-8
import os
import dashscope
from dashscope.audio.tts_v2 import *
# API キーはシンガポールリージョンと中国 (北京) リージョンで異なります。API キーの取得: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
# 環境変数を設定していない場合は、次の行を中国 Model Studio API キーに置き換えてください: dashscope.api_key = "sk-xxx"
dashscope.api_key = os.environ.get('DASHSCOPE_API_KEY')
# 以下はシンガポールリージョンの構成です。{WorkspaceId} を実際のワークスペース ID に置き換えてください。リージョンによって構成は異なります。
dashscope.base_websocket_api_url='wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'
# モデル
# モデルのバージョンによって対応する音声が異なります:
# cosyvoice-v3-flash/cosyvoice-v3-plus: longanyang などの音声を使用します。
# cosyvoice-v2: longxiaochun_v2 などの音声を使用します。
# 対象言語に適した音声を選択してください
model = "cosyvoice-v3-flash"
# 音声
voice = "longanhuan_v3"
# SpeechSynthesizer をインスタンス化し、コンストラクターで model や voice などのリクエストパラメーターを渡します
synthesizer = SpeechSynthesizer(model=model, voice=voice, instruction="请用河南话表达。")
# 合成するテキストを送信し、バイナリオーディオを取得します
audio = synthesizer.call("叫你去买盐,你买回来一袋面,这不是弄啥嘞吗!")
# 最初のテキスト送信時には WebSocket 接続を確立する必要があるため、最初のパケット遅延には接続設定時間が含まれます
print('[Metric] requestId: {}, first-packet latency: {} ms'.format(
synthesizer.get_last_request_id(),
synthesizer.get_first_package_delay()))
# オーディオをローカルファイルに保存します
with open('output.mp3', 'wb') as f:
f.write(audio)
Qwen-TTS
- システム音声: 組み込みの方言サポートを持つシステム音声を使用します。「サポートされている音声」の Qwen-TTS 音声リストをご参照ください。
サポート方言: 「Qwen3-TTS」の各モデルの「サポート言語」列をご参照ください。
感情およびリッチ言語タグ
Qwen-Audio-TTS シリーズモデルは、テキスト内に感情およびリッチ言語タグを直接埋め込むことで音声合成をサポートします(text パラメーター)。これらのタグは、指定された位置で感情表現を制御したり、笑い声やため息などの音声効果を挿入したりして、複雑なオーディオパラメーターを設定することなく、より表現豊かな音声を生成します。
重要サポートモデル: qwen-audio-3.0-tts-plus および qwen-audio-3.0-tts-flash のみ。
制限事項: 単方向ストリーミングモードのみをサポートします。
制御タグは、発話の感情やスタイルを設定します。テキスト内のタグを配置すると、次の制御タグが出現するか、文が長さによって自動的にセグメント化されるまで、その後のすべてのテキストに影響を与えます。
タグ | 説明 |
|---|---|
| 悲しい |
| 驚いた |
| 深く大きな声で叫ぶ |
| 震える |
| 怒り |
| 興奮した |
| 皮肉な |
| 好奇心旺盛な |
| ドラキュラ風(深く不気味な) |
| 退屈した |
| 疲れた |
| 軽蔑的な |
| 叫ぶ |
| ASMR のソフトなささやき |
| パニックになった |
| 悪戯っぽい |
| 共感的な |
| ささやく |
| 渋々と |
| 泣いている |
| 真剣な |
| 非常にゆっくり話す |
| 非常に速く話す |
リッチ言語タグは、テキスト内の現在位置に音声効果を挿入し、周囲のテキストの感情スタイルに影響を与えません。
タグ | 説明 |
|---|---|
| 息をのむ |
| ため息 |
| 喉払い |
| くすくす笑い |
| 笑い声 |
| 咳 |
| Snort |
以下の例は、text パラメーター内で制御タグとリッチ言語タグを組み合わせる方法を示しています。
[excited]What a beautiful day today![laughing]Let's go out and have fun together!
このテキストでは、[excited] は制御タグで、その後のすべてのテキストに興奮した感情を適用します。[laughing] はリッチ言語タグで、その位置に笑い声を挿入して、残りのテキストの合成を続けます。
同じテキスト内で異なる感情を切り替えることもできます:
[serious]Please pay attention to the safety precautions.[excited]Alright, let's get started now!
ここでは、[serious] が最初の文を真剣なトーンに設定し、[excited] が2番目の文から興奮したトーンに切り替えます。
タスクのキャンセル
リアルタイム音声合成中に現在の合成ターンを中断する必要がある場合は、キャンセルコマンドを送信します。キャンセル後、サーバーは直ちに現在のタスクを終了し、完了イベントを返します。再接続せずに同じ WebSocket 接続で新しい合成タスクを開始できます。
使用方法:
- Python SDK: 1.26.4 以降、
SpeechSynthesizer.streaming_cancel()を呼び出します。 - Java SDK: 2.22.26 以降、
SpeechSynthesizer.streamingCancel()を呼び出します。 - WebSocket 生プロトコル:
finish-taskイベントを送信し、input内のdirective=cancelを設定します。
重要モデルの制限事項:
- 中国 (北京): すべての Qwen-Audio-TTS モデルがこの機能をサポートします。CosyVoice モデルは v2 以降が必要です。
- シンガポール: すべての Qwen-Audio-TTS モデルがこの機能をサポートします。CosyVoice モデルはこの機能をサポートしていません。
WebSocket 生プロトコル呼び出し
以下の例は、DashScope SDK を使用せずに WebSocket 生プロトコルを介してサーバーに直接接続する方法を示しています。これらは最小限の実行可能な実装です。WebSocket プロトコルの詳細については、各モデルの API リファレンスをご参照ください。
WebSocket 生プロトコル呼び出し例の表示
Qwen-Audio-TTS/CosyVoice
Qwen-Audio-TTS と Qwen-Audio-TTS/CosyVoice は同じ WebSocket プロトコルを使用します。以下の例は qwen-audio-3.0-tts-flash を使用しています。Qwen-Audio-TTS/CosyVoice を使用するには、model パラメーターを Qwen-Audio-TTS/CosyVoice モデル(例: cosyvoice-v3-flash)に、voice パラメーターを希望の音声に置き換えてください。
Go
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"time"
"github.com/google/uuid"
"github.com/gorilla/websocket"
)
const (
// 以下はシンガポールリージョンの構成です。{WorkspaceId} を実際のワークスペース ID に置き換えてください。リージョンによって構成は異なります。
wsURL = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference"
outputFile = "output.mp3"
)
func main() {
// API キーはシンガポールリージョンと中国 (北京) リージョンで異なります。API キーの取得: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// 環境変数を設定していない場合は、次の行を中国 Model Studio API キーに置き換えてください: apiKey := "sk-xxx"
apiKey := os.Getenv("DASHSCOPE_API_KEY")
// 出力ファイルをクリアします
os.Remove(outputFile)
os.Create(outputFile)
// WebSocket に接続します
header := make(http.Header)
header.Add("X-DashScope-DataInspection", "enable")
header.Add("Authorization", fmt.Sprintf("bearer %s", apiKey))
conn, resp, err := websocket.DefaultDialer.Dial(wsURL, header)
if err != nil {
if resp != nil {
fmt.Printf("Connection failed, HTTP status code: %d\n", resp.StatusCode)
}
fmt.Println("Connection failed:", err)
return
}
defer conn.Close()
// タスク ID を生成します
taskID := uuid.New().String()
fmt.Printf("Generated task ID: %s\n", taskID)
// run-task イベントを送信します
runTaskCmd := map[string]interface{}{
"header": map[string]interface{}{
"action": "run-task",
"task_id": taskID,
"streaming": "duplex",
},
"payload": map[string]interface{}{
"task_group": "audio",
"task": "tts",
"function": "SpeechSynthesizer",
"model": "qwen-audio-3.0-tts-flash",
"parameters": map[string]interface{}{
"text_type": "PlainText",
"voice": "longanhuan_v3.6",
"format": "mp3",
"sample_rate": 22050,
"volume": 50,
"rate": 1,
"pitch": 1,
// enable_ssml を true に設定すると、continue-task イベントを 1 回しか送信できません。それ以外の場合、「Text request limit violated, expected 1.」というエラーが返されます
"enable_ssml": false,
},
"input": map[string]interface{}{},
},
}
runTaskJSON, _ := json.Marshal(runTaskCmd)
fmt.Printf("Sending run-task event: %s\n", string(runTaskJSON))
err = conn.WriteMessage(websocket.TextMessage, runTaskJSON)
if err != nil {
fmt.Println("Failed to send run-task:", err)
return
}
textSent := false
// メッセージを処理します
for {
messageType, message, err := conn.ReadMessage()
if err != nil {
fmt.Println("Failed to read message:", err)
break
}
// バイナリメッセージを処理します
if messageType == websocket.BinaryMessage {
fmt.Printf("Received binary message, length: %d\n", len(message))
file, _ := os.OpenFile(outputFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
file.Write(message)
file.Close()
continue
}
// テキストメッセージを処理します
messageStr := string(message)
fmt.Printf("Received text message: %s\n", strings.ReplaceAll(messageStr, "\n", ""))
// JSON を解析してイベントタイプを取得します
var msgMap map[string]interface{}
if json.Unmarshal(message, &msgMap) == nil {
if header, ok := msgMap["header"].(map[string]interface{}); ok {
if event, ok := header["event"].(string); ok {
fmt.Printf("Event type: %s\n", event)
switch event {
case "task-started":
fmt.Println("=== Received task-started event ===")
if !textSent {
// continue-task イベントを送信します
texts := []string{"Before my bed, moonlight shines bright, I suspect it's frost upon the ground.", "I raise my eyes to gaze at the bright moon, then bow my head, thinking of home."}
for _, text := range texts {
continueTaskCmd := map[string]interface{}{
"header": map[string]interface{}{
"action": "continue-task",
"task_id": taskID,
"streaming": "duplex",
},
"payload": map[string]interface{}{
"input": map[string]interface{}{
"text": text,
},
},
}
continueTaskJSON, _ := json.Marshal(continueTaskCmd)
fmt.Printf("Sending continue-task event: %s\n", string(continueTaskJSON))
err = conn.WriteMessage(websocket.TextMessage, continueTaskJSON)
if err != nil {
fmt.Println("Failed to send continue-task:", err)
return
}
}
textSent = true
// finish-task を送信する前に遅延を挿入します
time.Sleep(500 * time.Millisecond)
// finish-task イベントを送信します
finishTaskCmd := map[string]interface{}{
"header": map[string]interface{}{
"action": "finish-task",
"task_id": taskID,
"streaming": "duplex",
},
"payload": map[string]interface{}{
"input": map[string]interface{}{},
},
}
finishTaskJSON, _ := json.Marshal(finishTaskCmd)
fmt.Printf("Sending finish-task event: %s\n", string(finishTaskJSON))
err = conn.WriteMessage(websocket.TextMessage, finishTaskJSON)
if err != nil {
fmt.Println("Failed to send finish-task:", err)
return
}
}
case "task-finished":
fmt.Println("=== Task completed ===")
return
case "task-failed":
fmt.Println("=== Task failed ===")
if header["error_message"] != nil {
fmt.Printf("Error message: %s\n", header["error_message"])
}
return
case "result-generated":
fmt.Println("Received result-generated event")
}
}
}
}
}
}
C#
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
class Program {
// API キーはシンガポールリージョンと中国 (北京) リージョンで異なります。API キーの取得: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// 環境変数を設定していない場合は、次の行を中国 Model Studio API キーに置き換えてください: private static readonly string ApiKey = "sk-xxx"
private static readonly string ApiKey = Environment.GetEnvironmentVariable("DASHSCOPE_API_KEY") ?? throw new InvalidOperationException("DASHSCOPE_API_KEY environment variable is not set.");
// 以下はシンガポールリージョンの構成です。{WorkspaceId} を実際のワークスペース ID に置き換えてください。リージョンによって構成は異なります。
private const string WebSocketUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference";
// 出力ファイルパス
private const string OutputFilePath = "output.mp3";
// WebSocket クライアント
private static ClientWebSocket _webSocket = new ClientWebSocket();
// キャンセルトークンソース
private static CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
// タスク ID
private static string? _taskId;
// タスクが開始されたかどうか
private static TaskCompletionSource<bool> _taskStartedTcs = new TaskCompletionSource<bool>();
static async Task Main(string[] args) {
try {
// 出力ファイルをクリアします
ClearOutputFile(OutputFilePath);
// WebSocket サービスに接続します
await ConnectToWebSocketAsync(WebSocketUrl);
// メッセージ受信タスクを開始します
Task receiveTask = ReceiveMessagesAsync();
// run-task イベントを送信します
_taskId = GenerateTaskId();
await SendRunTaskCommandAsync(_taskId);
// task-started イベントを待ちます
await _taskStartedTcs.Task;
// continue-task イベントを送信します
string[] texts = {
"Before my bed, moonlight shines bright,",
"I suspect it\'s frost upon the ground.",
"I raise my eyes to gaze at the bright moon,",
"then bow my head, thinking of home."
};
foreach (string text in texts) {
await SendContinueTaskCommandAsync(text);
}
// finish-task イベントを送信します
await SendFinishTaskCommandAsync(_taskId);
// 受信タスクの完了を待ちます
await receiveTask;
Console.WriteLine("Task completed, connection closed.");
} catch (OperationCanceledException) {
Console.WriteLine("Task cancelled.");
} catch (Exception ex) {
Console.WriteLine($"Error occurred: {ex.Message}");
} finally {
_cancellationTokenSource.Cancel();
_webSocket.Dispose();
}
}
private static void ClearOutputFile(string filePath) {
if (File.Exists(filePath)) {
File.WriteAllText(filePath, string.Empty);
Console.WriteLine("Output file cleared.");
} else {
Console.WriteLine("Output file does not exist, no need to clear.");
}
}
private static async Task ConnectToWebSocketAsync(string url) {
var uri = new Uri(url);
if (_webSocket.State == WebSocketState.Connecting || _webSocket.State == WebSocketState.Open) {
return;
}
// WebSocket 接続ヘッダーを設定します
_webSocket.Options.SetRequestHeader("Authorization", $"bearer {ApiKey}");
_webSocket.Options.SetRequestHeader("X-DashScope-DataInspection", "enable");
try {
await _webSocket.ConnectAsync(uri, _cancellationTokenSource.Token);
Console.WriteLine("Successfully connected to the WebSocket service.");
} catch (OperationCanceledException) {
Console.WriteLine("WebSocket connection cancelled.");
} catch (Exception ex) {
Console.WriteLine($"WebSocket connection failed: {ex.Message}");
throw;
}
}
private static async Task SendRunTaskCommandAsync(string taskId) {
var command = CreateCommand("run-task", taskId, "duplex", new {
task_group = "audio",
task = "tts",
function = "SpeechSynthesizer",
model = "qwen-audio-3.0-tts-flash",
parameters = new
{
text_type = "PlainText",
voice = "longanhuan_v3.6",
format = "mp3",
sample_rate = 22050,
volume = 50,
rate = 1,
pitch = 1,
// enable_ssml を true に設定すると、continue-task イベントを 1 回しか送信できません。それ以外の場合、「Text request limit violated, expected 1.」というエラーが返されます
enable_ssml = false
},
input = new { }
});
await SendJsonMessageAsync(command);
Console.WriteLine("run-task event sent.");
}
private static async Task SendContinueTaskCommandAsync(string text) {
if (_taskId == null) {
throw new InvalidOperationException("Task ID not initialized.");
}
var command = CreateCommand("continue-task", _taskId, "duplex", new {
input = new {
text
}
});
await SendJsonMessageAsync(command);
Console.WriteLine("continue-task event sent.");
}
private static async Task SendFinishTaskCommandAsync(string taskId) {
var command = CreateCommand("finish-task", taskId, "duplex", new {
input = new { }
});
await SendJsonMessageAsync(command);
Console.WriteLine("finish-task event sent.");
}
private static async Task SendJsonMessageAsync(string message) {
var buffer = Encoding.UTF8.GetBytes(message);
try {
await _webSocket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, _cancellationTokenSource.Token);
} catch (OperationCanceledException) {
Console.WriteLine("Message sending cancelled.");
}
}
private static async Task ReceiveMessagesAsync() {
while (_webSocket.State == WebSocketState.Open) {
var response = await ReceiveMessageAsync();
if (response != null) {
var eventStr = response.RootElement.GetProperty("header").GetProperty("event").GetString();
switch (eventStr) {
case "task-started":
Console.WriteLine("Task started.");
_taskStartedTcs.TrySetResult(true);
break;
case "task-finished":
Console.WriteLine("Task completed.");
_cancellationTokenSource.Cancel();
break;
case "task-failed":
Console.WriteLine("Task failed: " + response.RootElement.GetProperty("header").GetProperty("error_message").GetString());
_cancellationTokenSource.Cancel();
break;
default:
// result-generated はここで処理できます
break;
}
}
}
}
private static async Task<JsonDocument?> ReceiveMessageAsync() {
var buffer = new byte[1024 * 4];
var segment = new ArraySegment<byte>(buffer);
try {
WebSocketReceiveResult result = await _webSocket.ReceiveAsync(segment, _cancellationTokenSource.Token);
if (result.MessageType == WebSocketMessageType.Close) {
await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", _cancellationTokenSource.Token);
return null;
}
if (result.MessageType == WebSocketMessageType.Binary) {
// バイナリデータを処理します
Console.WriteLine("Received binary data...");
// バイナリデータをファイルに保存します
using (var fileStream = new FileStream(OutputFilePath, FileMode.Append)) {
fileStream.Write(buffer, 0, result.Count);
}
return null;
}
string message = Encoding.UTF8.GetString(buffer, 0, result.Count);
return JsonDocument.Parse(message);
} catch (OperationCanceledException) {
Console.WriteLine("Message receiving cancelled.");
return null;
}
}
private static string GenerateTaskId() {
return Guid.NewGuid().ToString("N").Substring(0, 32);
}
private static string CreateCommand(string action, string taskId, string streaming, object payload) {
var command = new {
header = new {
action,
task_id = taskId,
streaming
},
payload
};
return JsonSerializer.Serialize(command);
}
}
PHP
サンプルコードのディレクトリ構造:
my-php-project/
├── composer.json
├── vendor/
└── index.php
composer.json の内容(依存関係のバージョンは必要に応じて調整):
{
"require": {
"react/event-loop": "^1.3",
"react/socket": "^1.11",
"react/stream": "^1.2",
"react/http": "^1.1",
"ratchet/pawl": "^0.4"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
index.php の内容:
<?php
require __DIR__ . '/vendor/autoload.php';
use Ratchet\Client\Connector;
use React\EventLoop\Loop;
use React\Socket\Connector as SocketConnector;
// API キーはシンガポールリージョンと中国 (北京) リージョンで異なります。API キーの取得: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// 環境変数を設定していない場合は、次の行を中国 Model Studio API キーに置き換えてください: $api_key = "sk-xxx"
$api_key = getenv("DASHSCOPE_API_KEY");
// 以下はシンガポールリージョンの構成です。{WorkspaceId} を実際のワークスペース ID に置き換えてください。リージョンによって構成は異なります。
$websocket_url = 'wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'; // WebSocket サーバー URL
$output_file = 'output.mp3'; // 出力ファイルパス
$loop = Loop::get();
if (file_exists($output_file)) {
// ファイル内容をクリアします
file_put_contents($output_file, '');
}
// カスタムコネクタを作成します
$socketConnector = new SocketConnector($loop, [
'tcp' => [
'bindto' => '0.0.0.0:0',
],
'tls' => [
'verify_peer' => false,
'verify_peer_name' => false,
],
]);
$connector = new Connector($loop, $socketConnector);
$headers = [
'Authorization' => 'bearer ' . $api_key,
'X-DashScope-DataInspection' => 'enable'
];
$connector($websocket_url, [], $headers)->then(function ($conn) use ($loop, $output_file) {
echo "Connected to the WebSocket server\n";
// タスク ID を生成します
$taskId = generateTaskId();
// run-task イベントを送信します
sendRunTaskMessage($conn, $taskId);
// continue-task イベントを送信する関数を定義します
$sendContinueTask = function() use ($conn, $loop, $taskId) {
// 送信するテキスト
$texts = ["Before my bed, moonlight shines bright,", "I suspect it\'s frost upon the ground.", "I raise my eyes to gaze at the bright moon,", "then bow my head, thinking of home."];
$continueTaskCount = 0;
foreach ($texts as $text) {
$continueTaskMessage = json_encode([
"header" => [
"action" => "continue-task",
"task_id" => $taskId,
"streaming" => "duplex"
],
"payload" => [
"input" => [
"text" => $text
]
]
]);
echo "Sending continue-task event: " . $continueTaskMessage . "\n";
$conn->send($continueTaskMessage);
$continueTaskCount++;
}
echo "Number of continue-task events sent: " . $continueTaskCount . "\n";
// finish-task イベントを送信します
sendFinishTaskMessage($conn, $taskId);
};
// task-started イベントを受信したかどうかのフラグ
$taskStarted = false;
// メッセージをリッスンします
$conn->on('message', function($msg) use ($conn, $sendContinueTask, $loop, &$taskStarted, $taskId, $output_file) {
if ($msg->isBinary()) {
// バイナリデータをローカルファイルに書き込みます
file_put_contents($output_file, $msg->getPayload(), FILE_APPEND);
} else {
// バイナリ以外のメッセージを処理します
$response = json_decode($msg, true);
if (isset($response['header']['event'])) {
handleEvent($conn, $response, $sendContinueTask, $loop, $taskId, $taskStarted);
} else {
echo "Unknown message format\n";
}
}
});
// 接続クローズをリッスンします
$conn->on('close', function($code = null, $reason = null) {
echo "Connection closed\n";
if ($code !== null) {
echo "Close code: " . $code . "\n";
}
if ($reason !== null) {
echo "Close reason: " . $reason . "\n";
}
});
}, function ($e) {
echo "Unable to connect: {$e->getMessage()}\n";
});
$loop->run();
/**
* タスク ID を生成します
* @return string
*/
function generateTaskId(): string {
return bin2hex(random_bytes(16));
}
/**
* run-task イベントを送信します
* @param $conn
* @param $taskId
*/
function sendRunTaskMessage($conn, $taskId) {
$runTaskMessage = json_encode([
"header" => [
"action" => "run-task",
"task_id" => $taskId,
"streaming" => "duplex"
],
"payload" => [
"task_group" => "audio",
"task" => "tts",
"function" => "SpeechSynthesizer",
"model" => "qwen-audio-3.0-tts-flash",
"parameters" => [
"text_type" => "PlainText",
"voice" => "longanhuan_v3.6",
"format" => "mp3",
"sample_rate" => 22050,
"volume" => 50,
"rate" => 1,
"pitch" => 1,
// enable_ssml を true に設定すると、continue-task イベントを 1 回しか送信できません。それ以外の場合、「Text request limit violated, expected 1.」というエラーが返されます
"enable_ssml" => false
],
"input" => (object) []
]
]);
echo "Sending run-task event: " . $runTaskMessage . "\n";
$conn->send($runTaskMessage);
echo "run-task event sent\n";
}
/**
* オーディオファイルを読み込みます
* @param string $filePath
* @return bool|string
*/
function readAudioFile(string $filePath) {
$voiceData = file_get_contents($filePath);
if ($voiceData === false) {
echo "Unable to read audio file\n";
}
return $voiceData;
}
/**
* オーディオデータを分割します
* @param string $data
* @param int $chunkSize
* @return array
*/
function splitAudioData(string $data, int $chunkSize): array {
return str_split($data, $chunkSize);
}
/**
* finish-task イベントを送信します
* @param $conn
* @param $taskId
*/
function sendFinishTaskMessage($conn, $taskId) {
$finishTaskMessage = json_encode([
"header" => [
"action" => "finish-task",
"task_id" => $taskId,
"streaming" => "duplex"
],
"payload" => [
"input" => (object) []
]
]);
echo "Sending finish-task event: " . $finishTaskMessage . "\n";
$conn->send($finishTaskMessage);
echo "finish-task event sent\n";
}
/**
* イベントを処理します
* @param $conn
* @param $response
* @param $sendContinueTask
* @param $loop
* @param $taskId
* @param $taskStarted
*/
function handleEvent($conn, $response, $sendContinueTask, $loop, $taskId, &$taskStarted) {
switch ($response['header']['event']) {
case 'task-started':
echo "Task started, sending continue-task events...\n";
$taskStarted = true;
// continue-task イベントを送信します
$sendContinueTask();
break;
case 'result-generated':
// result-generated イベントを受信しました
break;
case 'task-finished':
echo "Task completed\n";
$conn->close();
break;
case 'task-failed':
echo "Task failed\n";
echo "Error code: " . $response['header']['error_code'] . "\n";
echo "Error message: " . $response['header']['error_message'] . "\n";
$conn->close();
break;
case 'error':
echo "Error: " . $response['payload']['message'] . "\n";
break;
default:
echo "Unknown event: " . $response['header']['event'] . "\n";
break;
}
// タスクが完了した場合、接続をクローズします
if ($response['header']['event'] == 'task-finished') {
// すべてのデータが送信されたことを確認するために 1 秒待ちます
$loop->addTimer(1, function() use ($conn) {
$conn->close();
echo "Client closing connection\n";
});
}
// task-started イベントを受信していない場合、タスクが失敗またはエラーになったら接続をクローズします
if (!$taskStarted && in_array($response['header']['event'], ['task-failed', 'error'])) {
$conn->close();
}
}
Node.js
必要な依存関係をインストールします:
npm install ws
npm install uuid
サンプルコード:
const WebSocket = require('ws');
const fs = require('fs');
const uuid = require('uuid').v4;
// API キーはシンガポールリージョンと中国 (北京) リージョンで異なります。API キーの取得: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// 環境変数を設定していない場合は、次の行を中国 Model Studio API キーに置き換えてください: const apiKey = "sk-xxx"
const apiKey = process.env.DASHSCOPE_API_KEY;
// 以下はシンガポールリージョンの構成です。{WorkspaceId} を実際のワークスペース ID に置き換えてください。リージョンによって構成は異なります。
const url = 'wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference';
// 出力ファイルパス
const outputFilePath = 'output.mp3';
// 出力ファイルをクリアします
fs.writeFileSync(outputFilePath, '');
// WebSocket クライアントを作成します
const ws = new WebSocket(url, {
headers: {
Authorization: `bearer ${apiKey}`,
'X-DashScope-DataInspection': 'enable'
}
});
let taskStarted = false;
let taskId = uuid();
ws.on('open', () => {
console.log('Connected to the WebSocket server');
// run-task イベントを送信します
const runTaskMessage = JSON.stringify({
header: {
action: 'run-task',
task_id: taskId,
streaming: 'duplex'
},
payload: {
task_group: 'audio',
task: 'tts',
function: 'SpeechSynthesizer',
model: 'qwen-audio-3.0-tts-flash',
parameters: {
text_type: 'PlainText',
voice: 'longanhuan_v3.6', // 音声
format: 'mp3', // オーディオフォーマット
sample_rate: 22050, // サンプルレート
volume: 50, // ボリューム
rate: 1, // 発話速度
pitch: 1, // ピッチ
enable_ssml: false // SSML を有効にするかどうか。enable_ssml を true に設定すると、continue-task イベントを 1 回しか送信できません。それ以外の場合、「Text request limit violated, expected 1.」というエラーが返されます
},
input: {}
}
});
ws.send(runTaskMessage);
console.log('run-task message sent');
});
const fileStream = fs.createWriteStream(outputFilePath, { flags: 'a' });
ws.on('message', (data, isBinary) => {
if (isBinary) {
// バイナリデータをファイルに書き込みます
fileStream.write(data);
} else {
const message = JSON.parse(data);
switch (message.header.event) {
case 'task-started':
taskStarted = true;
console.log('Task started');
// continue-task イベントを送信します
sendContinueTasks(ws);
break;
case 'task-finished':
console.log('Task completed');
ws.close();
fileStream.end(() => {
console.log('File stream closed');
});
break;
case 'task-failed':
console.error('Task failed: ', message.header.error_message);
ws.close();
fileStream.end(() => {
console.log('File stream closed');
});
break;
default:
// result-generated はここで処理できます
break;
}
}
});
function sendContinueTasks(ws) {
const texts = [
'Before my bed, moonlight shines bright,',
'I suspect it is frost upon the ground.',
'I raise my eyes to gaze at the bright moon,',
'then bow my head, thinking of home.'
];
texts.forEach((text, index) => {
setTimeout(() => {
if (taskStarted) {
const continueTaskMessage = JSON.stringify({
header: {
action: 'continue-task',
task_id: taskId,
streaming: 'duplex'
},
payload: {
input: {
text: text
}
}
});
ws.send(continueTaskMessage);
console.log(`continue-task sent, text: ${text}`);
}
}, index * 1000); // 1 秒ごとに送信します
});
// finish-task イベントを送信します
setTimeout(() => {
if (taskStarted) {
const finishTaskMessage = JSON.stringify({
header: {
action: 'finish-task',
task_id: taskId,
streaming: 'duplex'
},
payload: {
input: {}
}
});
ws.send(finishTaskMessage);
console.log('finish-task sent');
}
}, texts.length * 1000 + 1000); // すべての continue-task イベント送信後 1 秒で送信します
}
ws.on('close', () => {
console.log('Disconnected from the WebSocket server');
});
Java
開発には Java DashScope SDK の使用を推奨します。「Qwen-Audio-TTS Java SDK / CosyVoice Java SDK」をご参照ください。
以下は Java WebSocket 直接接続の例です。実行前に以下の依存関係をインポートしてください:
Java-WebSocketjackson-databind
Maven または Gradle を使用して依存関係を管理します:
<dependencies>
<!-- WebSocket Client -->
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.5.3</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.0</version>
</dependency>
</dependencies>
// 他のコードは省略
dependencies {
// WebSocket Client
implementation 'org.java-websocket:Java-WebSocket:1.5.3'
// JSON Processing
implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.0'
}
// 他のコードは省略
Java コード:
import com.fasterxml.jackson.databind.ObjectMapper;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.*;
public class TTSWebSocketClient extends WebSocketClient {
private final String taskId = UUID.randomUUID().toString();
private final String outputFile = "output_" + System.currentTimeMillis() + ".mp3";
private boolean taskFinished = false;
public TTSWebSocketClient(URI serverUri, Map<String, String> headers) {
super(serverUri, headers);
}
@Override
public void onOpen(ServerHandshake serverHandshake) {
System.out.println("Connection established");
// run-task イベントを送信します
// enable_ssml を true に設定すると、continue-task イベントを 1 回しか送信できません。それ以外の場合、「Text request limit violated, expected 1.」というエラーが返されます
String runTaskCommand = "{ \"header\": { \"action\": \"run-task\", \"task_id\": \"" + taskId + "\", \"streaming\": \"duplex\" }, \"payload\": { \"task_group\": \"audio\", \"task\": \"tts\", \"function\": \"SpeechSynthesizer\", \"model\": \"qwen-audio-3.0-tts-flash\", \"parameters\": { \"text_type\": \"PlainText\", \"voice\": \"longanhuan_v3.6\", \"format\": \"mp3\", \"sample_rate\": 22050, \"volume\": 50, \"rate\": 1, \"pitch\": 1, \"enable_ssml\": false }, \"input\": {} }}";
send(runTaskCommand);
}
@Override
public void onMessage(String message) {
System.out.println("Received message from server: " + message);
try {
// JSON メッセージを解析します
Map<String, Object> messageMap = new ObjectMapper().readValue(message, Map.class);
if (messageMap.containsKey("header")) {
Map<String, Object> header = (Map<String, Object>) messageMap.get("header");
if (header.containsKey("event")) {
String event = (String) header.get("event");
if ("task-started".equals(event)) {
System.out.println("Received task-started event from server");
List<String> texts = Arrays.asList(
"Before my bed, moonlight shines bright,I suspect it\'s frost upon the ground.",
"I raise my eyes to gaze at the bright moon,then bow my head, thinking of home."
);
for (String text : texts) {
// continue-task イベントを送信します
sendContinueTask(text);
}
// finish-task イベントを送信します
sendFinishTask();
} else if ("task-finished".equals(event)) {
System.out.println("Received task-finished event from server");
taskFinished = true;
closeConnection();
} else if ("task-failed".equals(event)) {
System.out.println("Task failed: " + message);
closeConnection();
}
}
}
} catch (Exception e) {
System.err.println("Exception occurred: " + e.getMessage());
}
}
@Override
public void onMessage(ByteBuffer message) {
System.out.println("Received binary audio data of size: " + message.remaining());
try (FileOutputStream fos = new FileOutputStream(outputFile, true)) {
byte[] buffer = new byte[message.remaining()];
message.get(buffer);
fos.write(buffer);
System.out.println("Audio data written to local file " + outputFile);
} catch (IOException e) {
System.err.println("Failed to write audio data to local file: " + e.getMessage());
}
}
@Override
public void onClose(int code, String reason, boolean remote) {
System.out.println("Connection closed: " + reason + " (" + code + ")");
}
@Override
public void onError(Exception ex) {
System.err.println("Error: " + ex.getMessage());
ex.printStackTrace();
}
private void sendContinueTask(String text) {
String command = "{ \"header\": { \"action\": \"continue-task\", \"task_id\": \"" + taskId + "\", \"streaming\": \"duplex\" }, \"payload\": { \"input\": { \"text\": \"" + text + "\" } }}";
send(command);
}
private void sendFinishTask() {
String command = "{ \"header\": { \"action\": \"finish-task\", \"task_id\": \"" + taskId + "\", \"streaming\": \"duplex\" }, \"payload\": { \"input\": {} }}";
send(command);
}
private void closeConnection() {
if (!isClosed()) {
close();
}
}
public static void main(String[] args) {
try {
// API キーはシンガポールリージョンと中国 (北京) リージョンで異なります。API キーの取得: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// 環境変数を設定していない場合は、次の行を中国 Model Studio API キーに置き換えてください: String apiKey = "sk-xxx"
String apiKey = System.getenv("DASHSCOPE_API_KEY");
if (apiKey == null || apiKey.isEmpty()) {
System.err.println("Please set the DASHSCOPE_API_KEY environment variable");
return;
}
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "bearer " + apiKey);
// 以下はシンガポールリージョンの構成です。{WorkspaceId} を実際のワークスペース ID に置き換えてください。リージョンによって構成は異なります。
TTSWebSocketClient client = new TTSWebSocketClient(new URI("wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference"), headers);
client.connect();
while (!client.isClosed() && !client.taskFinished) {
Thread.sleep(1000);
}
} catch (Exception e) {
System.err.println("Failed to connect to WebSocket service: " + e.getMessage());
e.printStackTrace();
}
}
}
Python
開発には Python DashScope SDK の使用を推奨します。「Qwen-Audio-TTS Python SDK / CosyVoice Python SDK」をご参照ください。
以下は Python WebSocket 直接接続の例です。実行前に以下の依存関係をインストールしてください:
pip uninstall websocket-client
pip uninstall websocket
pip install websocket-client
重要Python ファイル名を "websocket.py" にしないでください。そうするとエラーが発生します(AttributeError: module 'websocket' has no attribute 'WebSocketApp'. Did you mean: 'WebSocket'?)。
import websocket
import json
import uuid
import os
import time
class TTSClient:
def __init__(self, api_key, uri):
"""
TTSClient インスタンスを初期化します。
パラメーター:
api_key (str): 認証用の API キー
uri (str): WebSocket サービス URL
"""
self.api_key = api_key # ご自身の API キーに置き換えてください
self.uri = uri # ご自身の WebSocket URL に置き換えてください
self.task_id = str(uuid.uuid4()) # ユニークなタスク ID を生成します
self.output_file = f"output_{int(time.time())}.mp3" # 出力オーディオファイルのパス
self.ws = None # WebSocketApp インスタンス
self.task_started = False # task-started を受信したかどうか
self.task_finished = False # task-finished / task-failed を受信したかどうか
def on_open(self, ws):
"""
WebSocket 接続が確立されたときのコールバックです。
音声合成タスクを開始するための run-task イベントを送信します。
"""
print("WebSocket connected")
# run-task イベントを構築します
run_task_cmd = {
"header": {
"action": "run-task",
"task_id": self.task_id,
"streaming": "duplex"
},
"payload": {
"task_group": "audio",
"task": "tts",
"function": "SpeechSynthesizer",
"model": "qwen-audio-3.0-tts-flash",
"parameters": {
"text_type": "PlainText",
"voice": "longanhuan_v3.6",
"format": "mp3",
"sample_rate": 22050,
"volume": 50,
"rate": 1,
"pitch": 1,
# enable_ssml を true に設定すると、continue-task イベントは 1 つしか送信できません。それ以外の場合はエラーが返されます。
"enable_ssml": False
},
"input": {}
}
}
# run-task イベントを送信します
ws.send(json.dumps(run_task_cmd))
print("Sent run-task event")
def on_message(self, ws, message):
"""
メッセージを受信したときのコールバックです。
テキストメッセージとバイナリメッセージを別々に処理します。
"""
if isinstance(message, str):
# JSON テキストメッセージを処理します
try:
msg_json = json.loads(message)
print(f"Received JSON message: {msg_json}")
if "header" in msg_json:
header = msg_json["header"]
if "event" in header:
event = header["event"]
if event == "task-started":
print("Task started")
self.task_started = True
# continue-task イベントを送信します
texts = [
"Before my bed, moonlight shines bright,I suspect it\'s frost upon the ground.",
"I raise my eyes to gaze at the bright moon,then bow my head, thinking of home."
]
for text in texts:
self.send_continue_task(text)
# すべての continue-task イベントを送信した後に finish-task を送信します
self.send_finish_task()
elif event == "task-finished":
print("Task completed")
self.task_finished = True
self.close(ws)
elif event == "task-failed":
error_msg = msg_json.get("error_message", "Unknown error")
print(f"Task failed: {error_msg}")
self.task_finished = True
self.close(ws)
except json.JSONDecodeError as e:
print(f"JSON parsing failed: {e}")
else:
# バイナリメッセージ (オーディオデータ) を処理します
print(f"Received binary message, size: {len(message)} bytes")
with open(self.output_file, "ab") as f:
f.write(message)
print(f"Audio data written to local file {self.output_file}")
def on_error(self, ws, error):
"""エラーが発生したときのコールバックです。"""
print(f"WebSocket error: {error}")
def on_close(self, ws, close_status_code, close_msg):
"""接続が閉じられたときのコールバックです。"""
print(f"WebSocket closed: {close_msg} ({close_status_code})")
def send_continue_task(self, text):
"""合成対象のテキストコンテンツを含む continue-task イベントを送信します。"""
cmd = {
"header": {
"action": "continue-task",
"task_id": self.task_id,
"streaming": "duplex"
},
"payload": {
"input": {
"text": text
}
}
}
self.ws.send(json.dumps(cmd))
print(f"Sent continue-task event, text content: {text}")
def send_finish_task(self):
"""音声合成タスクを終了する finish-task イベントを送信します。"""
cmd = {
"header": {
"action": "finish-task",
"task_id": self.task_id,
"streaming": "duplex"
},
"payload": {
"input": {}
}
}
self.ws.send(json.dumps(cmd))
print("Sent finish-task event")
def close(self, ws):
"""接続をアクティブに閉じます。"""
if ws and ws.sock and ws.sock.connected:
ws.close()
print("Connection closed actively")
def run(self):
"""WebSocket クライアントを開始します。"""
# リクエストヘッダーを設定します (認証)
header = {
"Authorization": f"bearer {self.api_key}",
"X-DashScope-DataInspection": "enable"
}
# WebSocketApp インスタンスを作成します
self.ws = websocket.WebSocketApp(
self.uri,
header=header,
on_open=self.on_open,
on_message=self.on_message,
on_error=self.on_error,
on_close=self.on_close
)
print("Listening for WebSocket messages...")
self.ws.run_forever() # 持続的接続リスナーを開始します
# 使用例
if __name__ == "__main__":
# API キーはシンガポールリージョンと北京リージョンで異なります。API キーの取得: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
# 環境変数を設定していない場合は、次の行を中国語版 Model Studio の API キーに置き換えてください: API_KEY = "sk-xxx"
API_KEY = os.environ.get("DASHSCOPE_API_KEY")
# 以下はシンガポールリージョンの設定です。{WorkspaceId} を実際のワークスペース ID に置き換えてください。設定はリージョンによって異なります。
SERVER_URI = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference" # ご自身の WebSocket URL に置き換えてください
client = TTSClient(API_KEY, SERVER_URI)
client.run()
Qwen-TTS
-
クライアントの作成
Python
tts_realtime_client.pyという名前の Python ファイルを作成し、以下のコードをファイルにコピーします:# -- coding: utf-8 -- import asyncio import websockets import json import base64 import time from typing import Optional, Callable, Dict, Any from enum import Enum class SessionMode(Enum): SERVER_COMMIT = "server_commit" COMMIT = "commit" class TTSRealtimeClient: """ TTS リアルタイム API とやり取りするためのクライアント。 このクラスは、TTS リアルタイム API への接続、テキストデータの送信、 オーディオ出力の受信、WebSocket 接続の管理を行うメソッドを提供します。 Attributes: base_url (str): リアルタイム API のベース URL。 api_key (str): 認証用の API キー。 voice (str): サーバー側音声合成で使用する音声。 mode (SessionMode): セッションモード。server_commit または commit。 audio_callback (Callable[[bytes], None]): オーディオデータを受信するためのコールバック関数。 language_type(str) 合成音声の言語。オプション: 中国語、英語、ドイツ語、イタリア語、ポルトガル語、スペイン語、日本語、韓国語、フランス語、ロシア語、Auto """ def __init__( self, base_url: str, api_key: str, voice: str = "Cherry", mode: SessionMode = SessionMode.SERVER_COMMIT, audio_callback: Optional[Callable[[bytes], None]] = None, language_type: str = "Auto"): self.base_url = base_url self.api_key = api_key self.voice = voice self.mode = mode self.ws = None self.audio_callback = audio_callback self.language_type = language_type # 現在の応答状態 self._current_response_id = None self._current_item_id = None self._is_responding = False self._response_done_future = None async def connect(self) -> None: """TTS リアルタイム API との WebSocket 接続を確立します。""" headers = { "Authorization": f"Bearer {self.api_key}" } self.ws = await websockets.connect(self.base_url, additional_headers=headers) # デフォルトのセッション構成を設定します await self.update_session({ "mode": self.mode.value, "voice": self.voice, "language_type": self.language_type, "response_format": "pcm", "sample_rate": 24000 }) async def send_event(self, event) -> None: """サーバーにイベントを送信します。""" event['event_id'] = "event_" + str(int(time.time() * 1000)) print(f"Sending event: type={event['type']}, event_id={event['event_id']}") await self.ws.send(json.dumps(event)) async def update_session(self, config: Dict[str, Any]) -> None: """セッション構成を更新します。""" event = { "type": "session.update", "session": config } print("Updating session configuration: ", event) await self.send_event(event) async def append_text(self, text: str) -> None: """API にテキストデータを送信します。""" event = { "type": "input_text_buffer.append", "text": text } await self.send_event(event) async def commit_text_buffer(self) -> None: """テキストバッファーをコミットして処理をトリガーします。""" event = { "type": "input_text_buffer.commit" } await self.send_event(event) async def clear_text_buffer(self) -> None: """テキストバッファーをクリアします。""" event = { "type": "input_text_buffer.clear" } await self.send_event(event) async def finish_session(self) -> None: """セッションを終了します。""" event = { "type": "session.finish" } await self.send_event(event) async def wait_for_response_done(self): """response.done イベントを待ちます""" if self._response_done_future: await self._response_done_future async def handle_messages(self) -> None: """サーバーからのメッセージを処理します。""" try: async for message in self.ws: event = json.loads(message) event_type = event.get("type") if event_type != "response.audio.delta": print(f"Received event: {event_type}") if event_type == "error": print("Error: ", event.get('error', {})) continue elif event_type == "session.created": print("Session created, ID: ", event.get('session', {}).get('id')) elif event_type == "session.updated": print("Session updated, ID: ", event.get('session', {}).get('id')) elif event_type == "input_text_buffer.committed": print("Text buffer committed, item ID: ", event.get('item_id')) elif event_type == "input_text_buffer.cleared": print("Text buffer cleared") elif event_type == "response.created": self._current_response_id = event.get("response", {}).get("id") self._is_responding = True # response.done を待つための新しい future を作成します self._response_done_future = asyncio.Future() print("Response created, ID: ", self._current_response_id) elif event_type == "response.output_item.added": self._current_item_id = event.get("item", {}).get("id") print("Output item added, ID: ", self._current_item_id) # オーディオデルタを処理します elif event_type == "response.audio.delta" and self.audio_callback: audio_bytes = base64.b64decode(event.get("delta", "")) self.audio_callback(audio_bytes) elif event_type == "response.audio.done": print("Audio generation completed") elif event_type == "response.done": self._is_responding = False self._current_response_id = None self._current_item_id = None # future を完了としてマークします if self._response_done_future and not self._response_done_future.done(): self._response_done_future.set_result(True) print("Response completed") elif event_type == "session.finished": print("Session finished") except websockets.exceptions.ConnectionClosed: print("Connection closed") except Exception as e: print("Error handling messages: ", str(e)) async def close(self) -> None: """WebSocket 接続をクローズします。""" if self.ws: await self.ws.close()Java
TTSRealtimeClient.javaという名前の Java ファイルを作成し、以下のコードをファイルにコピーします:import com.google.gson.Gson; import com.google.gson.JsonObject; import org.java_websocket.client.WebSocketClient; import org.java_websocket.handshake.ServerHandshake; import java.net.URI; import java.util.Base64; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.function.Consumer; /** * TTS リアルタイム API とやり取りするためのクライアント。 * * このクラスは、TTS リアルタイム API への接続、テキストデータの送信、オーディオ出力の取得、WebSocket 接続の管理を行うメソッドを提供します。 */ public class TTSRealtimeClient { public enum SessionMode { SERVER_COMMIT("server_commit"), COMMIT("commit"); private final String value; SessionMode(String value) { this.value = value; } public String getValue() { return value; } } /** * オーディオコールバックインターフェイス */ public interface AudioCallback { void onAudio(byte[] audioData); } private final String baseUrl; private final String apiKey; private final String voice; private final SessionMode mode; private final String languageType; private final AudioCallback audioCallback; private final Gson gson = new Gson(); private WebSocketClient ws; private CountDownLatch responseDoneLatch; private CountDownLatch sessionFinishedLatch; public TTSRealtimeClient(String baseUrl, String apiKey, String voice, SessionMode mode, AudioCallback audioCallback, String languageType) { this.baseUrl = baseUrl; this.apiKey = apiKey; this.voice = voice; this.mode = mode; this.audioCallback = audioCallback; this.languageType = languageType; } public TTSRealtimeClient(String baseUrl, String apiKey, String voice, SessionMode mode, AudioCallback audioCallback) { this(baseUrl, apiKey, voice, mode, audioCallback, "Auto"); } /** * TTS リアルタイム API への WebSocket 接続を確立します。 */ public void connect() throws Exception { Map<String, String> headers = new HashMap<>(); headers.put("Authorization", "Bearer " + apiKey); responseDoneLatch = new CountDownLatch(0); sessionFinishedLatch = new CountDownLatch(1); ws = new WebSocketClient(new URI(baseUrl), headers) { @Override public void onOpen(ServerHandshake handshake) { System.out.println("WebSocket connection established"); // デフォルトのセッション構成を送信します JsonObject session = new JsonObject(); session.addProperty("mode", mode.getValue()); session.addProperty("voice", TTSRealtimeClient.this.voice); session.addProperty("language_type", languageType); session.addProperty("response_format", "pcm"); session.addProperty("sample_rate", 24000); updateSession(session); } @Override public void onMessage(String message) { JsonObject event = gson.fromJson(message, JsonObject.class); String eventType = event.has("type") ? event.get("type").getAsString() : ""; if (!"response.audio.delta".equals(eventType)) { System.out.println("Received event: " + eventType); } switch (eventType) { case "error": System.err.println("Error: " + event.get("error")); break; case "session.created": System.out.println("Session created, ID: " + event.getAsJsonObject("session").get("id").getAsString()); break; case "session.updated": System.out.println("Session updated, ID: " + event.getAsJsonObject("session").get("id").getAsString()); break; case "input_text_buffer.committed": System.out.println("Text buffer committed, item ID: " + event.get("item_id")); break; case "input_text_buffer.cleared": System.out.println("Text buffer cleared"); break; case "response.created": System.out.println("Response created, ID: " + event.getAsJsonObject("response").get("id").getAsString()); responseDoneLatch = new CountDownLatch(1); break; case "response.output_item.added": System.out.println("Output item added, ID: " + event.getAsJsonObject("item").get("id").getAsString()); break; case "response.audio.delta": if (audioCallback != null) { byte[] audioBytes = Base64.getDecoder().decode( event.get("delta").getAsString()); audioCallback.onAudio(audioBytes); } break; case "response.audio.done": System.out.println("Audio generation completed"); break; case "response.done": System.out.println("Response completed"); responseDoneLatch.countDown(); break; case "session.finished": System.out.println("Session finished"); sessionFinishedLatch.countDown(); break; } } @Override public void onClose(int code, String reason, boolean remote) { System.out.println("Connection closed: " + reason); } @Override public void onError(Exception ex) { System.err.println("WebSocket error: " + ex.getMessage()); } }; ws.connectBlocking(); } /** * サーバーにイベントを送信します。 */ public void sendEvent(JsonObject event) { String eventId = "event_" + System.currentTimeMillis(); event.addProperty("event_id", eventId); System.out.println("Sending event: type=" + event.get("type").getAsString() + ", event_id=" + eventId); ws.send(gson.toJson(event)); } /** * セッション構成を更新します。 */ public void updateSession(JsonObject config) { JsonObject event = new JsonObject(); event.addProperty("type", "session.update"); event.add("session", config); System.out.println("Updating session configuration: " + event); sendEvent(event); } /** * API にテキストデータを送信します。 */ public void appendText(String text) { JsonObject event = new JsonObject(); event.addProperty("type", "input_text_buffer.append"); event.addProperty("text", text); sendEvent(event); } /** * テキストバッファーをコミットして処理をトリガーします。 */ public void commitTextBuffer() { JsonObject event = new JsonObject(); event.addProperty("type", "input_text_buffer.commit"); sendEvent(event); } /** * テキストバッファーをクリアします。 */ public void clearTextBuffer() { JsonObject event = new JsonObject(); event.addProperty("type", "input_text_buffer.clear"); sendEvent(event); } /** * セッションを終了します。 */ public void finishSession() { JsonObject event = new JsonObject(); event.addProperty("type", "session.finish"); sendEvent(event); } /** * response.done イベントを待ちます。 */ public void waitForResponseDone() throws InterruptedException { responseDoneLatch.await(); } /** * session.finished イベントを待ちます。 */ public void waitForSessionFinished() throws InterruptedException { sessionFinishedLatch.await(); } /** * WebSocket 接続をクローズします。 */ public void close() { if (ws != null) { ws.close(); } } } -
音声合成モードの選択
リアルタイム API は 2 つのモードをサポートしています:
-
サーバーコミットモード
サーバーがテキスト分割と合成タイミングを自動的に処理します。クライアントはテキストを送信するだけです。低遅延が求められるシナリオ(GPS ナビゲーションなど)に適しています。
-
コミットモード
クライアントがバッファーにテキストを追加し、明示的に合成をトリガーします。ニュース放送など、文の分割制御を正確に行う必要があるシナリオに適しています。
サーバーコミットモード
Python
tts_realtime_client.pyと同じディレクトリに、server_commit.pyという名前の別の Python ファイルを作成し、以下のコードをファイルにコピーします:import os import asyncio import logging import wave from tts_realtime_client import TTSRealtimeClient, SessionMode import pyaudio # QwenTTS サービス構成 # 以下はシンガポールリージョンの構成です。 URL = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime?model=qwen3-tts-flash-realtime" # API キーはシンガポールリージョンと北京リージョンで異なります。API キーの取得: https://www.alibabacloud.com/help/zh/model-studio/get-api-key # 環境変数を設定していない場合は、次の行を中国 Model Studio API キーに置き換えてください: API_KEY="sk-xxx" API_KEY = os.getenv("DASHSCOPE_API_KEY") if not API_KEY: raise ValueError("Please set DASHSCOPE_API_KEY environment variable") # オーディオデータを収集します _audio_chunks = [] # リアルタイム再生関連 _AUDIO_SAMPLE_RATE = 24000 _audio_pyaudio = pyaudio.PyAudio() _audio_stream = None # 実行時にオープンされます def _audio_callback(audio_bytes: bytes): """TTSRealtimeClient オーディオコールバック: リアルタイム再生とキャッシュ""" global _audio_stream if _audio_stream is not None: try: _audio_stream.write(audio_bytes) except Exception as exc: logging.error(f"PyAudio playback error: {exc}") _audio_chunks.append(audio_bytes) logging.info(f"Received audio chunk: {len(audio_bytes)} bytes") def _save_audio_to_file(filename: str = "output.wav", sample_rate: int = 24000) -> bool: """収集したオーディオデータを WAV ファイルとして保存します""" if not _audio_chunks: logging.warning("No audio data to save") return False try: audio_data = b"".join(_audio_chunks) with wave.open(filename, 'wb') as wav_file: wav_file.setnchannels(1) # モノラル wav_file.setsampwidth(2) # 16 ビット wav_file.setframerate(sample_rate) wav_file.writeframes(audio_data) logging.info(f"Audio saved to: {filename}") return True except Exception as exc: logging.error(f"Failed to save audio: {exc}") return False async def _produce_text(client: TTSRealtimeClient): """サーバーにテキストフラグメントを送信します""" text_fragments = [ "Alibaba Cloud の大規模言語モデルプラットフォームである Model Studio は、大規模言語モデルアプリケーションを開発・構築するためのオールインワンプラットフォームです。", "開発者とビジネスユーザーの両方が、大規模言語モデルアプリケーションの設計と開発に深く関与できます。", "シンプルなインターフェイスを使用して、5 分で大規模言語モデルアプリケーションを開発できます。", "または、数時間で専用モデルをトレーニングでき、アプリケーションの革新により多くのエネルギーを注げます。", ] logging.info("Sending text fragments…") for text in text_fragments: logging.info(f"Sending fragment: {text}") await client.append_text(text) await asyncio.sleep(0.1) # フラグメント間の短い遅延 # サーバーが内部処理を完了するのを待ってからセッションを終了します await asyncio.sleep(1.0) await client.finish_session() async def _run_demo(): """完全なデモを実行します""" global _audio_stream # PyAudio 出力ストリームを開きます _audio_stream = _audio_pyaudio.open( format=pyaudio.paInt16, channels=1, rate=_AUDIO_SAMPLE_RATE, output=True, frames_per_buffer=1024 ) client = TTSRealtimeClient( base_url=URL, api_key=API_KEY, voice="Cherry", mode=SessionMode.SERVER_COMMIT, audio_callback=_audio_callback ) # 接続を確立します await client.connect() # メッセージ処理とテキスト送信を並行して実行します consumer_task = asyncio.create_task(client.handle_messages()) producer_task = asyncio.create_task(_produce_text(client)) await producer_task # テキスト送信の完了を待ちます # response.done を待ちます await client.wait_for_response_done() # 接続をクローズし、コンシューマータスクをキャンセルします await client.close() consumer_task.cancel() # オーディオストリームをクローズします if _audio_stream is not None: _audio_stream.stop_stream() _audio_stream.close() _audio_pyaudio.terminate() # オーディオデータを保存します os.makedirs("outputs", exist_ok=True) _save_audio_to_file(os.path.join("outputs", "qwen_tts_output.wav")) def main(): """同期エントリポイント""" logging.basicConfig( level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) logging.info("Starting QwenTTS Realtime Client demo…") asyncio.run(_run_demo()) if __name__ == "__main__": main()server_commit.pyを実行して、リアルタイム API によって生成されたオーディオをリアルタイムで聞きます。Java
TTSRealtimeClient.javaと同じディレクトリに、ServerCommit.javaという名前の別の Java ファイルを作成し、以下のコードをファイルにコピーします:import javax.sound.sampled.*; import java.io.*; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicBoolean; public class ServerCommit { // 以下はシンガポールリージョンの構成です。 private static final String URL = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime?model=qwen3-tts-flash-realtime"; // API キーはシンガポールリージョンと中国 (北京) リージョンで異なります。API キーの取得: https://www.alibabacloud.com/help/zh/model-studio/get-api-key // 環境変数を設定していない場合は、次の行を中国 Model Studio API キーに置き換えてください: private static final String API_KEY = "sk-xxx"; private static final String API_KEY = System.getenv("DASHSCOPE_API_KEY"); private static final int SAMPLE_RATE = 24000; // オーディオデータキャッシュ private static final List<byte[]> audioChunks = new ArrayList<>(); // リアルタイム再生キュー private static final ConcurrentLinkedQueue<byte[]> playbackQueue = new ConcurrentLinkedQueue<>(); private static final AtomicBoolean playing = new AtomicBoolean(true); public static void main(String[] args) throws Exception { if (API_KEY == null || API_KEY.isEmpty()) { throw new IllegalStateException("Please set the DASHSCOPE_API_KEY environment variable"); } // オーディオ再生を初期化します AudioFormat format = new AudioFormat(SAMPLE_RATE, 16, 1, true, false); DataLine.Info info = new DataLine.Info(SourceDataLine.class, format); SourceDataLine audioLine = (SourceDataLine) AudioSystem.getLine(info); audioLine.open(format); audioLine.start(); // 再生スレッドを開始します Thread playerThread = new Thread(() -> { while (playing.get() || !playbackQueue.isEmpty()) { byte[] chunk = playbackQueue.poll(); if (chunk != null) { audioLine.write(chunk, 0, chunk.length); } else { try { Thread.sleep(10); } catch (InterruptedException ignored) {} } } }); playerThread.start(); // TTS クライアントを作成します TTSRealtimeClient client = new TTSRealtimeClient( URL, API_KEY, "Cherry", TTSRealtimeClient.SessionMode.SERVER_COMMIT, audioData -> { playbackQueue.add(audioData); audioChunks.add(audioData); System.out.println("Received audio data: " + audioData.length + " bytes"); } ); client.connect(); // テキストフラグメントを送信します String[] textFragments = { "Alibaba Cloud の大規模言語モデルプラットフォームである Model Studio は、大規模言語モデルアプリケーションを開発・構築するためのオールインワンプラットフォームです。", "開発者とビジネスユーザーの両方が、大規模言語モデルアプリケーションの設計と開発に深く関与できます。", "シンプルなインターフェイスを使用して、5 分で大規模言語モデルアプリケーションを開発できます。", "または、数時間で専用モデルをトレーニングでき、アプリケーションの革新により多くのエネルギーを注げます。" }; System.out.println("Starting to send text..."); for (String text : textFragments) { System.out.println("Sending fragment: " + text); client.appendText(text); Thread.sleep(100); } Thread.sleep(1000); client.finishSession(); // 応答の完了を待ちます client.waitForResponseDone(); client.waitForSessionFinished(); client.close(); // 再生の完了を待ちます playing.set(false); playerThread.join(); audioLine.drain(); audioLine.close(); // オーディオファイルを保存します saveWav("output.wav"); System.out.println("Done"); } private static void saveWav(String filename) throws IOException { if (audioChunks.isEmpty()) { System.out.println("No audio data to save"); return; } ByteArrayOutputStream bos = new ByteArrayOutputStream(); for (byte[] chunk : audioChunks) { bos.write(chunk); } byte[] allAudio = bos.toByteArray(); AudioFormat format = new AudioFormat(SAMPLE_RATE, 16, 1, true, false); AudioInputStream ais = new AudioInputStream( new ByteArrayInputStream(allAudio), format, allAudio.length / 2); new File("outputs").mkdirs(); AudioSystem.write(ais, AudioFileFormat.Type.WAVE, new File("outputs/" + filename)); System.out.println("Audio saved to: outputs/" + filename); } }ServerCommit.javaをコンパイルして実行し、リアルタイム API によって生成されたオーディオをリアルタイムで聞きます。コミットモード
Python
tts_realtime_client.pyと同じディレクトリに、commit.pyという名前の別の Python ファイルを作成し、以下のコードをファイルにコピーします:import os import asyncio import logging import wave from tts_realtime_client import TTSRealtimeClient, SessionMode import pyaudio # QwenTTS サービスの構成 # 以下はシンガポールリージョンの構成です。 URL = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime?model=qwen3-tts-flash-realtime" # API キーはシンガポールリージョンと北京リージョンで異なります。API キーの取得: https://www.alibabacloud.com/help/zh/model-studio/get-api-key # 環境変数を構成していない場合は、次の行を中国 Model Studio API キーに置き換えてください: API_KEY="sk-xxx" API_KEY = os.getenv("DASHSCOPE_API_KEY") if not API_KEY: raise ValueError("DASHSCOPE_API_KEY 環境変数を設定してください") # 音声データを収集 _audio_chunks = [] _AUDIO_SAMPLE_RATE = 24000 _audio_pyaudio = pyaudio.PyAudio() _audio_stream = None def _audio_callback(audio_bytes: bytes): """TTSRealtimeClient 音声コールバック: リアルタイム再生とキャッシュ""" global _audio_stream if _audio_stream is not None: try: _audio_stream.write(audio_bytes) except Exception as exc: logging.error(f"PyAudio 再生エラー: {exc}") _audio_chunks.append(audio_bytes) logging.info(f"受信した音声チャンク: {len(audio_bytes)} バイト") def _save_audio_to_file(filename: str = "output.wav", sample_rate: int = 24000) -> bool: """収集した音声データを WAV ファイルとして保存""" if not _audio_chunks: logging.warning("保存する音声データがありません") return False try: audio_data = b"".join(_audio_chunks) with wave.open(filename, 'wb') as wav_file: wav_file.setnchannels(1) # モノラル wav_file.setsampwidth(2) # 16 ビット wav_file.setframerate(sample_rate) wav_file.writeframes(audio_data) logging.info(f"音声を保存しました: {filename}") return True except Exception as exc: logging.error(f"音声の保存に失敗しました: {exc}") return False async def _user_input_loop(client: TTSRealtimeClient): """ユーザー入力を継続的に取得してテキストを送信します。ユーザーが空のテキストを入力した場合、コミットイベントを送信して現在のセッションを終了します""" print("テキストを入力してください (直接 Enter キーを押してコミットイベントを送信し、現在のセッションを終了します。プログラムを終了するには Ctrl+C または Ctrl+D を押してください):") while True: try: user_text = input("> ") if not user_text: # ユーザー入力が空 # 空の入力は会話の終了として扱われます: コミットバッファー -> セッション終了 -> ループ中断 logging.info("空の入力です。コミットイベントを送信して現在のセッションを終了します") await client.commit_text_buffer() # サーバーがコミットを処理する時間を確保し、音声を失う可能性のある早期のセッション終了を防ぎます await asyncio.sleep(0.3) await client.finish_session() break # ユーザー入力ループを直接終了し、再度 Enter キーを押す必要はありません else: logging.info(f"テキストを送信します: {user_text}") await client.append_text(user_text) except EOFError: # ユーザーが Ctrl+D を押しました break except KeyboardInterrupt: # ユーザーが Ctrl+C を押しました break # セッション終了 logging.info("セッションを終了しています...") async def _run_demo(): """完全なデモを実行します""" global _audio_stream # PyAudio 出力ストリームを開く _audio_stream = _audio_pyaudio.open( format=pyaudio.paInt16, channels=1, rate=_AUDIO_SAMPLE_RATE, output=True, frames_per_buffer=1024 ) client = TTSRealtimeClient( base_url=URL, api_key=API_KEY, voice="Cherry", mode=SessionMode.COMMIT, # COMMIT モードに変更 audio_callback=_audio_callback ) # 接続を確立 await client.connect() # メッセージ処理とユーザー入力を並行して実行 consumer_task = asyncio.create_task(client.handle_messages()) producer_task = asyncio.create_task(_user_input_loop(client)) await producer_task # ユーザー入力の完了を待ちます # 応答完了を待ちます await client.wait_for_response_done() # 接続を閉じてコンシューマータスクをキャンセル await client.close() consumer_task.cancel() # 音声ストリームを閉じる if _audio_stream is not None: _audio_stream.stop_stream() _audio_stream.close() _audio_pyaudio.terminate() # 音声データを保存 os.makedirs("outputs", exist_ok=True) _save_audio_to_file(os.path.join("outputs", "qwen_tts_output.wav")) def main(): logging.basicConfig( level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) logging.info("QwenTTS リアルタイムクライアントデモを開始しています…") asyncio.run(_run_demo()) if __name__ == "__main__": main()commit.pyを実行します。複数回テキストを入力して合成できます。テキストを入力せずに Enter を押すと、リアルタイム API から返されたオーディオがスピーカーから再生されます。Java
TTSRealtimeClient.javaと同じディレクトリに、Commit.javaという名前の別の Java ファイルを作成し、以下のコードをファイルにコピーします:import javax.sound.sampled.*; import java.io.*; import java.util.ArrayList; import java.util.List; import java.util.Scanner; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicBoolean; public class Commit { // 以下はシンガポールリージョンの構成です。 private static final String URL = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime?model=qwen3-tts-flash-realtime"; // API キーはシンガポールリージョンと中国 (北京) リージョンで異なります。API キーの取得: https://www.alibabacloud.com/help/zh/model-studio/get-api-key // 環境変数を設定していない場合は、次の行を中国 Model Studio API キーに置き換えてください: private static final String API_KEY = "sk-xxx"; private static final String API_KEY = System.getenv("DASHSCOPE_API_KEY"); private static final int SAMPLE_RATE = 24000; private static final List<byte[]> audioChunks = new ArrayList<>(); private static final ConcurrentLinkedQueue<byte[]> playbackQueue = new ConcurrentLinkedQueue<>(); private static final AtomicBoolean playing = new AtomicBoolean(true); public static void main(String[] args) throws Exception { if (API_KEY == null || API_KEY.isEmpty()) { throw new IllegalStateException("Please set the DASHSCOPE_API_KEY environment variable"); } // オーディオ再生を初期化します AudioFormat format = new AudioFormat(SAMPLE_RATE, 16, 1, true, false); DataLine.Info info = new DataLine.Info(SourceDataLine.class, format); SourceDataLine audioLine = (SourceDataLine) AudioSystem.getLine(info); audioLine.open(format); audioLine.start(); // 再生スレッドを開始します Thread playerThread = new Thread(() -> { while (playing.get() || !playbackQueue.isEmpty()) { byte[] chunk = playbackQueue.poll(); if (chunk != null) { audioLine.write(chunk, 0, chunk.length); } else { try { Thread.sleep(10); } catch (InterruptedException ignored) {} } } }); playerThread.start(); // TTS クライアントを作成します(コミットモード) TTSRealtimeClient client = new TTSRealtimeClient( URL, API_KEY, "Cherry", TTSRealtimeClient.SessionMode.COMMIT, audioData -> { playbackQueue.add(audioData); audioChunks.add(audioData); System.out.println("Received audio data: " + audioData.length + " bytes"); } ); client.connect(); // インタラクティブ入力 System.out.println("Enter text (press Enter directly to send a commit event and end the session, press Ctrl+D to exit the program):"); Scanner scanner = new Scanner(System.in); while (true) { System.out.print("> "); if (!scanner.hasNextLine()) { client.finishSession(); break; } String userText = scanner.nextLine(); if (userText.isEmpty()) { // 空の入力: バッファーをコミットしてセッションを終了します System.out.println("Empty input, sending commit event and ending the session"); client.commitTextBuffer(); Thread.sleep(300); client.finishSession(); break; } else { System.out.println("Sending text: " + userText); client.appendText(userText); } } scanner.close(); // 応答の完了を待ちます client.waitForResponseDone(); client.waitForSessionFinished(); client.close(); // 再生の完了を待ちます playing.set(false); playerThread.join(); audioLine.drain(); audioLine.close(); // オーディオファイルを保存します saveWav("output.wav"); System.out.println("Done"); } private static void saveWav(String filename) throws IOException { if (audioChunks.isEmpty()) { System.out.println("No audio data to save"); return; } ByteArrayOutputStream bos = new ByteArrayOutputStream(); for (byte[] chunk : audioChunks) { bos.write(chunk); } byte[] allAudio = bos.toByteArray(); AudioFormat format = new AudioFormat(SAMPLE_RATE, 16, 1, true, false); AudioInputStream ais = new AudioInputStream( new ByteArrayInputStream(allAudio), format, allAudio.length / 2); new File("outputs").mkdirs(); AudioSystem.write(ais, AudioFileFormat.Type.WAVE, new File("outputs/" + filename)); System.out.println("Audio saved to: outputs/" + filename); } }Commit.javaをコンパイルして実行します。複数回テキストを入力して合成できます。テキストを入力せずに Enter を押すと、リアルタイム API から返されたオーディオがスピーカーから再生されます。 -
サーバーコミットモード
本番環境に適用
接続再利用 (WebSocket)
WebSocket 接続は再利用可能です。合成タスクが完了後、再接続せずに同じ接続で次のタスクを開始できます。
再利用プロセス:
- Qwen-Audio-TTS / Qwen-Audio-TTS/CosyVoice: クライアントが
finish-taskを送信し、サーバーがtask-finishedを返した後、クライアントはrun-taskを送信して新しいタスクを開始できます。 - Qwen-TTS: クライアントが
session.finishを送信し、サーバーがsession.finishedを返した後、クライアントは新しいセッションを作成して次のタスクを開始できます。
キャンセル後の再利用: Qwen-Audio-TTS / Qwen-Audio-TTS/CosyVoice の場合、現在のタスクを cancel ディレクティブを使用してキャンセルした後も、サーバーが task-finished を返した後、同じ接続で新しい run-task を送信できます。詳細については、「タスクのキャンセル」をご参照ください。
重要
- 新しいタスクを開始する前に、サーバーが完了イベント(
task-finishedまたはsession.finished)を返すのを待ってください。 - Qwen-Audio-TTS、 Qwen-Audio-TTS/CosyVoice は、再利用接続上の各タスクに対して異なる
task_idを必要とします。 - タスクが失敗した場合、サーバーはエラーイベントを返して接続をクローズします。この接続は再利用できません。
- 前のタスク終了後 60 秒以内に新しいタスクを開始しない場合、接続は自動的にクローズされます。
各モデルのイベント詳細については、対応する「API リファレンス」をご参照ください。
レート制限
モデル呼び出しはレート制限の対象となります。制限を超えた場合、サーバーはエラー Requests rate limit exceeded, please try again later. を返します。リクエストレートまたは同時実行数を下げて再試行してください。
各モデルのレート制限については、「レート制限」をご参照ください。
高同時実行時のベストプラクティス
DashScope SDK には組み込みのプーリング機能があり、WebSocket 接続と synthesizer オブジェクトを再利用することで、繰り返しの作成と破棄に伴うオーバーヘッドを排除します。
高同時実行時のベストプラクティスの表示
Qwen-Audio-TTS/CosyVoice
Qwen-Audio-TTS と Qwen-Audio-TTS/CosyVoice は同じ SDK インターフェイスを使用します。以下の例は Qwen-Audio-TTS モデルにも適用されます。単に model および voice パラメーターを置き換えてください。
前提条件
-
DashScope SDK がインストールされており、バージョン要件を満たしていること。最新バージョンのインストールを推奨します:
- Python SDK: バージョン >= 1.25.2
- Java SDK: バージョン >= 2.16.6
Python SDK
Python SDK は SpeechSynthesizerObjectPool を使用して SpeechSynthesizer オブジェクトを管理および再利用します。
プールは初期化時に指定された数の SpeechSynthesizer インスタンスを作成し、WebSocket 接続を確立します。オブジェクトを借用すると、すぐにリクエストを送信できる状態になり、最初のパケット遅延が短縮されます。オブジェクトを返却後も接続はアクティブなまま維持され、次のタスクで再利用されます。
実装手順
-
依存関係のインストール: DashScope 依存関係をインストールします (
pip install -U dashscope)。 -
オブジェクトプールの作成と構成
プールサイズをピーク同時実行数の 1.5~2 倍に設定し、アカウントの QPS 制限を超えないようにしてください。
グローバルシングルトンプールを作成します(初期化時の接続確立には時間がかかります):
from dashscope.audio.tts_v2 import SpeechSynthesizerObjectPool
synthesizer_object_pool = SpeechSynthesizerObjectPool(max_size=20)
import dashscope
# 以下は中国 (北京) リージョンの構成です。{WorkspaceId} を実際のワークスペース ID に置き換えてください。リージョンによって構成は異なります。
dashscope.base_http_api_url = "https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1"
重要
- オブジェクトプールのシナリオでは、
SpeechSynthesizerObjectPoolは初期化時に現在のグローバルdashscope.api_keyを使用して WebSocket 接続をサーバーと確立します。API キーは WebSocket ハンドシェイク時の認証のためにのみAuthorizationヘッダーに書き込まれます。後続のタスクメッセージ(run-taskなど)は API キーを含みません。プール作成後にdashscope.api_keyを変更しても、既存の接続には影響しません。borrow_synthesizerで借用されたオブジェクト(返却後再借用されたものを含む)は、元のハンドシェイク時の API キーを引き続き使用します。新しい値は静かに無視され、ID、クォータ、課金の帰属が期待とは異なる可能性があります。注:borrow_synthesizerは API キーをパラメーターとして指定することをサポートしていません。 - 複数の API キーを使用する場合は、各キーに対して別々の
SpeechSynthesizerObjectPoolインスタンスを維持してください。
-
プールから
SpeechSynthesizerオブジェクトを借用返却されていないオブジェクトの数がプール容量を超える場合、システムは追加のオブジェクトを作成します。
これらの追加オブジェクトは新しい接続を確立する必要があり、プーリングの恩恵を受けません。
speech_synthesizer = connectionPool.borrow_synthesizer(
model='cosyvoice-v3-flash',
voice='longanyang',
seed=12382,
callback=synthesizer_callback
)
-
音声合成の実行
SpeechSynthesizerオブジェクトの call または streaming_call メソッドを呼び出して音声合成を行います。 -
SpeechSynthesizerオブジェクトの返却タスク完了後にオブジェクトを返却して、再利用可能にします。
未完了または失敗したタスクのオブジェクトは返却しないでください。
connectionPool.return_synthesizer(speech_synthesizer)
完全なコード
重要このコードを使用する前に: SpeechSynthesizerObjectPool は初期化時に現在のグローバル dashscope.api_key を使用して WebSocket 接続を確立および認証します。プール作成後に dashscope.api_key を変更しても、既存の接続には影響しません。新しい値は静かに無視されます。複数の API キーを使用する場合は、各キーに対して別々のプールインスタンスを維持してください。詳細については、上記の重要な注意事項をご参照ください。
# !/usr/bin/env python3
# Copyright (C) Alibaba Group. All Rights Reserved.
# MIT License (https://opensource.org/licenses/MIT)
import os
import time
import threading
import dashscope
from dashscope.audio.tts_v2 import *
USE_CONNECTION_POOL = True
text_to_synthesize = [
'Sentence 1: Welcome to Alibaba speech synthesis service.',
'Sentence 2: Welcome to Alibaba speech synthesis service.',
'Sentence 3: Welcome to Alibaba speech synthesis service.',
]
connectionPool = None
def init_dashscope_api_key():
'''
DashScope API キーを設定します。詳細情報:
https://github.com/aliyun/alibabacloud-bailian-speech-demo/blob/master/PREREQUISITES.md
'''
# API キーはシンガポールリージョンと北京リージョンで異なります。API キーの取得: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
if 'DASHSCOPE_API_KEY' in os.environ:
dashscope.api_key = os.environ[
'DASHSCOPE_API_KEY'] # 環境変数 DASHSCOPE_API_KEY から API キーを読み込みます
else:
dashscope.api_key = '<your-dashscope-api-key>' # API キーを手動で設定します
def synthesis_text_to_speech_and_play_by_streaming_mode(text, task_id):
global USE_CONNECTION_POOL, connectionPool
'''
ストリーミングモードで指定されたテキストから音声を合成し、非同期呼び出しで合成された音声をリアルタイムで再生します。
詳細については、https://www.alibabacloud.com/help/document_detail/2712523.html をご参照ください。
'''
complete_event = threading.Event()
# 結果を処理するためのコールバックを定義します
class Callback(ResultCallback):
def on_open(self):
# オブジェクトプールを使用する場合、on_open はタスク開始後に呼び出されます
self.file = open(f'result_{task_id}.mp3', 'wb')
print(f'[task_{task_id}] start')
def on_complete(self):
print(f'[task_{task_id}] 音声合成タスクが正常に完了しました。')
complete_event.set()
def on_error(self, message: str):
print(f'[task_{task_id}] 音声合成タスクが失敗しました。{message}')
def on_close(self):
# オブジェクトプールを使用する場合、on_open はタスク終了後に呼び出されます
print(f'[task_{task_id}] finished')
def on_event(self, message):
# print(f'recv speech synthsis message {message}')
pass
def on_data(self, data: bytes) -> None:
# プレーヤーに送信します
# 音声をファイルに保存します
self.file.write(data)
# 音声合成コールバックを呼び出します
synthesizer_callback = Callback()
# 音声合成器を初期化します
# 音声、フォーマット、サンプルレートなどの合成パラメーターをカスタマイズできます
if USE_CONNECTION_POOL:
speech_synthesizer = connectionPool.borrow_synthesizer(
model='cosyvoice-v3-flash',
voice='longanyang',
seed=12382,
callback=synthesizer_callback
)
else:
speech_synthesizer = SpeechSynthesizer(model='cosyvoice-v3-flash',
voice='longanyang',
seed=12382,
callback=synthesizer_callback)
try:
speech_synthesizer.call(text)
except Exception as e:
print(f'[task_{task_id}] 音声合成タスクが失敗しました。{e}')
if USE_CONNECTION_POOL:
# 接続プールを使用する場合、タスクが失敗した場合は合成器の接続を手動で閉じます。
speech_synthesizer.close()
return
print('[task_{}] 合成テキスト: {}'.format(task_id, text))
complete_event.wait()
print('[task_{}][Metric] requestId: {}, first package delay ms: {}'.format(
task_id,
speech_synthesizer.get_last_request_id(),
speech_synthesizer.get_first_package_delay()))
if USE_CONNECTION_POOL:
connectionPool.return_synthesizer(speech_synthesizer)
# main function
if __name__ == '__main__':
# SpeechSynthesizerObjectPool を作成する前に、dashscope.api_key と base_websocket_api_url を設定する必要があります。
# プールは初期化時に現在のグローバル dashscope.api_key を使用して WebSocket 接続を確立します。
# プール作成後に dashscope.api_key を変更しても、プール内の既存の接続には影響しません。
# 以下はシンガポールリージョンの構成です。"{WorkspaceId}" を実際のワークスペース ID に置き換えてください。構成はリージョンによって異なります。
dashscope.base_websocket_api_url='wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'
init_dashscope_api_key()
if USE_CONNECTION_POOL:
print('creating connection pool')
start_time = time.time() * 1000
connectionPool = SpeechSynthesizerObjectPool(max_size=3)
end_time = time.time() * 1000
print('connection pool created, cost: {} ms'.format(end_time - start_time))
task_thread_list = []
for task_id in range(3):
thread = threading.Thread(
target=synthesis_text_to_speech_and_play_by_streaming_mode,
args=(text_to_synthesize[task_id], task_id))
task_thread_list.append(thread)
for task_thread in task_thread_list:
task_thread.start()
for task_thread in task_thread_list:
task_thread.join()
if USE_CONNECTION_POOL:
connectionPool.shutdown()
リソース管理とエラー処理
-
タスク成功時: 音声合成タスクが正常に完了した後、
connectionPool.return_synthesizer(speech_synthesizer)を呼び出してSpeechSynthesizerオブジェクトをプールに返却し、再利用可能にします。重要未完了または失敗したタスクの
SpeechSynthesizerオブジェクトは返却しないでください。 -
タスク失敗時: SDK 内部エラーまたはビジネスロジック例外によりタスクが中止された場合、基盤となる WebSocket 接続をクローズします:
speech_synthesizer.close() -
すべての合成タスクが完了した後、プールをシャットダウンします:
connectionPool.shutdown() -
サーバー側で TaskFailed エラーが発生した場合、追加の処理は不要です。
Java SDK
Java SDK は、組み込みの接続プールとカスタムオブジェクトプールの連携により最適なパフォーマンスを実現します。
- 接続プール: SDK に統合された OkHttp3 接続プールが基盤となる WebSocket 接続を管理および再利用し、ネットワークハンドシェイクのオーバーヘッドを削減します。これはデフォルトで有効になっています。
- オブジェクトプール:
commons-pool2上に構築され、事前に接続済みのSpeechSynthesizerオブジェクトのセットを維持します。プールから借用することで接続設定の遅延がなくなり、最初のパケット遅延が大幅に短縮されます。
実装手順
-
依存関係の追加
お使いのビルドツールに基づいて、プロジェクトの依存関係構成に dashscope-sdk-java および commons-pool2 を追加します。
Maven および Gradle の例:
Maven
- Maven プロジェクトの
pom.xmlファイルを開きます。 <dependencies>タグ内に以下の依存関係を追加します。
<dependency> <groupId>com.alibaba</groupId> <artifactId>dashscope-sdk-java</artifactId> <!-- 'the-latest-version' をバージョン 2.16.9 以降に置き換えてください。利用可能なバージョンは https://mvnrepository.com/artifact/com.alibaba/dashscope-sdk-java をご確認ください --> <version>the-latest-version</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <!-- 'the-latest-version' を最新バージョンに置き換えてください。利用可能なバージョンは https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 をご確認ください --> <version>the-latest-version</version> </dependency>pom.xmlファイルを保存します。mvn clean installまたはmvn compileなどの Maven コマンドを実行して、プロジェクトの依存関係を更新します。
Gradle
- Gradle プロジェクトの
build.gradleファイルを開きます。 dependenciesブロック内に以下の依存関係を追加します。
dependencies { // 'the-latest-version' をバージョン 2.16.6 以降に置き換えてください。利用可能なバージョンは https://mvnrepository.com/artifact/com.alibaba/dashscope-sdk-java をご確認ください implementation group: 'com.alibaba', name: 'dashscope-sdk-java', version: 'the-latest-version' // 'the-latest-version' を最新バージョンに置き換えてください。利用可能なバージョンは https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 をご確認ください implementation group: 'org.apache.commons', name: 'commons-pool2', version: 'the-latest-version' }build.gradleファイルを保存します。- ターミナルでプロジェクトルートディレクトリに移動し、以下の Gradle コマンドを実行して依存関係を更新します。
./gradlew build --refresh-dependenciesWindows の場合は、代わりに以下のコマンドを使用します:
gradlew build --refresh-dependencies - Maven プロジェクトの
-
接続プールの構成
環境変数を通じて主要な接続プールパラメーターを構成します:
環境変数 | 説明 |
|---|---|
DASHSCOPE_CONNECTION_POOL_SIZE | 接続プールサイズ。 推奨値: ピーク同時実行数の少なくとも 2 倍。 デフォルト: 32。 |
DASHSCOPE_MAXIMUM_ASYNC_REQUESTS | 非同期リクエストの最大数。 推奨値: デフォルト: 32。 |
DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST | ホストごとの非同期リクエストの最大数。 推奨値: デフォルト: 32。 |
-
オブジェクトプールの構成
環境変数を通じてオブジェクトプールサイズを構成します:
環境変数 | 説明 |
|---|---|
COSYVOICE_OBJECTPOOL_SIZE | オブジェクトプールサイズ。 推奨値: ピーク同時実行数の 1.5~2 倍。 デフォルト: 500。 |
重要
- オブジェクトプールサイズ (
COSYVOICE_OBJECTPOOL_SIZE) は接続プールサイズ (DASHSCOPE_CONNECTION_POOL_SIZE) 以下である必要があります。そうでない場合、オブジェクトプールがオブジェクトを要求し、接続プールがいっぱいになると、呼び出しスレッドが利用可能な接続を待機するためにブロックされます。 - オブジェクトプールサイズはアカウントの QPS (クエリ/秒) 制限を超えてはなりません。
以下のコードでオブジェクトプールを作成します:
class CosyvoiceObjectPool {
// ... 他のコードはここでは省略されています。完全な例については、完全なコードをご参照ください。
public static GenericObjectPool<SpeechSynthesizer> getInstance() {
lock.lock();
if (synthesizerPool == null) {
// ここでオブジェクトプールサイズを設定するか、環境変数 COSYVOICE_OBJECTPOOL_SIZE で設定できます。
// サーバーの最大同時接続数の 1.5~2 倍に設定することを推奨します。
int objectPoolSize = getObjectivePoolSize();
SpeechSynthesizerObjectFactory speechSynthesizerObjectFactory =
new SpeechSynthesizerObjectFactory();
GenericObjectPoolConfig<SpeechSynthesizer> config =
new GenericObjectPoolConfig<>();
config.setMaxTotal(objectPoolSize);
config.setMaxIdle(objectPoolSize);
config.setMinIdle(objectPoolSize);
synthesizerPool =
new GenericObjectPool<>(speechSynthesizerObjectFactory, config);
}
lock.unlock();
return synthesizerPool;
}
}
-
プールから
SpeechSynthesizerオブジェクトを借用返却されていないオブジェクトの数がプールの最大容量を超える場合、システムは追加の
SpeechSynthesizerオブジェクトを作成します。これらの新しく作成されたオブジェクトは再初期化と新しい WebSocket 接続を必要とし、プーリングの恩恵を受けません。
synthesizer = CosyvoiceObjectPool.getInstance().borrowObject();
-
音声合成の実行
プールから
SpeechSynthesizerオブジェクトを借用した後、updateParamAndCallback(param, callback)を呼び出して現在のタスクのパラメーターとコールバックをバインドし、その後streamingCallまたはcallを呼び出して音声合成を行います。重要
- オブジェクトプールのシナリオでは、
updateParamAndCallbackは複数回呼び出されます(オブジェクトを借用するたびに、voiceやformatなどのタスクレベルのパラメーターを設定するため)。各呼び出しで渡されるapiKeyは常に同じでなければなりません。updateParamAndCallbackは現在のSpeechSynthesizerインスタンスのローカルフィールドのみを更新し、基盤となる WebSocket 接続を再構築しません。SDK は WebSocket ハンドシェイク時の認証のためにのみapiKeyをAuthorizationヘッダーに書き込みます。後続のタスクメッセージ(run-taskなど)はapiKeyを含みません。再利用された接続が切断されない限り、新しいapiKeyを渡してもサーバーには送信されず、リクエストは引き続き初期ハンドシェイク時のapiKeyを使用します。これにより、ID、クォータ、課金の帰属が期待とは異なる可能性があります。 - 複数の API キーを使用する場合は、各キーに対して別々のオブジェクトプールインスタンスを維持してください。
- オブジェクトプールのシナリオでは、
-
SpeechSynthesizerオブジェクトの返却合成タスク完了後、
SpeechSynthesizerオブジェクトを返却して、後続のタスクで再利用可能にします。未完了または失敗したタスクのオブジェクトは返却しないでください。
CosyvoiceObjectPool.getInstance().returnObject(synthesizer);
完全なコード
重要このコードを使用する前に: オブジェクトプールのシナリオでは、updateParamAndCallback に渡される apiKey は複数回の呼び出しで常に同じでなければなりません。SDK は確立済みの接続の API キーを更新せず、異なる API キーを渡しても効果がありません。複数の API キーを使用する場合は、各キーに対して別々のプールインスタンスを維持してください。詳細については、上記の重要な注意事項をご参照ください。
import com.alibaba.dashscope.audio.tts.SpeechSynthesisResult;
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesisAudioFormat;
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesisParam;
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesizer;
import com.alibaba.dashscope.common.ResultCallback;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.alibaba.dashscope.utils.Constants;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
/**
* プロジェクトに org.apache.commons.pool2 および DashScope パッケージを含める必要があります。
*
* DashScope SDK 2.16.6 以降のバージョンは高同時実行シナリオ向けに最適化されています。
* DashScope SDK 2.16.6 より前のバージョンは高同時実行での使用は推奨されません。
*
*
* TTS サービスを高同時実行で呼び出す前に、
* 以下の環境変数を通じて接続プールパラメーターを構成してください。
*
* DASHSCOPE_MAXIMUM_ASYNC_REQUESTS
* DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST
* DASHSCOPE_CONNECTION_POOL_SIZE
*
*/
class SpeechSynthesizerObjectFactory
extends BasePooledObjectFactory<SpeechSynthesizer> {
public SpeechSynthesizerObjectFactory() {
super();
}
@Override
public SpeechSynthesizer create() throws Exception {
return new SpeechSynthesizer();
}
@Override
public PooledObject<SpeechSynthesizer> wrap(SpeechSynthesizer obj) {
return new DefaultPooledObject<>(obj);
}
}
class CosyvoiceObjectPool {
public static GenericObjectPool<SpeechSynthesizer> synthesizerPool;
public static String COSYVOICE_OBJECTPOOL_SIZE_ENV = "COSYVOICE_OBJECTPOOL_SIZE";
public static int DEFAULT_OBJECT_POOL_SIZE = 500;
private static Lock lock = new java.util.concurrent.locks.ReentrantLock();
public static int getObjectivePoolSize() {
try {
Integer n = Integer.parseInt(System.getenv(COSYVOICE_OBJECTPOOL_SIZE_ENV));
System.out.println("Using Object Pool Size In Env: "+ n);
return n;
} catch (NumberFormatException e) {
System.out.println("Using Default Object Pool Size: "+ DEFAULT_OBJECT_POOL_SIZE);
return DEFAULT_OBJECT_POOL_SIZE;
}
}
public static GenericObjectPool<SpeechSynthesizer> getInstance() {
lock.lock();
if (synthesizerPool == null) {
// ここでオブジェクトプールサイズを設定するか、COSYVOICE_OBJECTPOOL_SIZE 環境変数で設定できます。
// サーバーの最大同時接続数の 1.5~2 倍に設定することを推奨します。
int objectPoolSize = getObjectivePoolSize();
SpeechSynthesizerObjectFactory speechSynthesizerObjectFactory =
new SpeechSynthesizerObjectFactory();
GenericObjectPoolConfig<SpeechSynthesizer> config =
new GenericObjectPoolConfig<>();
config.setMaxTotal(objectPoolSize);
config.setMaxIdle(objectPoolSize);
config.setMinIdle(objectPoolSize);
synthesizerPool =
new GenericObjectPool<>(speechSynthesizerObjectFactory, config);
}
lock.unlock();
return synthesizerPool;
}
}
class SynthesizeTaskWithCallback implements Runnable {
String[] textArray;
String requestId;
long timeCost;
public SynthesizeTaskWithCallback(String[] textArray) {
this.textArray = textArray;
}
@Override
public void run() {
SpeechSynthesizer synthesizer = null;
long startTime = System.currentTimeMillis();
// onError を受信した場合
final boolean[] hasError = {false};
try {
class ReactCallback extends ResultCallback<SpeechSynthesisResult> {
ReactCallback() {}
@Override
public void onEvent(SpeechSynthesisResult message) {
if (message.getAudioFrame() != null) {
try {
byte[] bytesArray = message.getAudioFrame().array();
System.out.println("Received audio, audio stream length: " + bytesArray.length);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
@Override
public void onComplete() {}
@Override
public void onError(Exception e) {
System.out.println(e.getMessage());
e.printStackTrace();
hasError[0] = true;
}
}
SpeechSynthesisParam param =
SpeechSynthesisParam.builder()
.model("cosyvoice-v3-flash")
.voice("longanyang")
// API キーはシンガポールリージョンと北京リージョンで異なります。API キーの取得: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// 環境変数を設定していない場合は、次の行を中国 Model Studio API キーに置き換えてください: .apiKey("sk-xxx")
.apiKey(System.getenv("DASHSCOPE_API_KEY"))
.format(SpeechSynthesisAudioFormat
.MP3_22050HZ_MONO_256KBPS) // ストリーミング合成には PCM または MP3 を使用します
.build();
try {
synthesizer = CosyvoiceObjectPool.getInstance().borrowObject();
// 注: オブジェクトプールのシナリオでは、updateParamAndCallback への複数回の呼び出しで渡される apiKey は常に同じでなければなりません。SDK は確立済みの接続の apiKey を更新しないため、異なる apiKey を渡しても効果がありません。「音声合成の実行」ステップの上記の重要な注意事項をご参照ください。
synthesizer.updateParamAndCallback(param, new ReactCallback());
for (String text : textArray) {
synthesizer.streamingCall(text);
}
Thread.sleep(20);
synthesizer.streamingComplete(60000);
requestId = synthesizer.getLastRequestId();
} catch (Exception e) {
System.out.println("Exception e: " + e.toString());
hasError[0] = true;
}
} catch (Exception e) {
hasError[0] = true;
throw new RuntimeException(e);
}
if (synthesizer != null) {
try {
if (hasError[0] == true) {
// 例外が発生した場合、接続をクローズしてプール内のオブジェクトを無効化します。
synthesizer.getDuplexApi().close(1000, "bye");
CosyvoiceObjectPool.getInstance().invalidateObject(synthesizer);
} else {
// タスクが正常に完了した場合、オブジェクトをプールに返却します。
CosyvoiceObjectPool.getInstance().returnObject(synthesizer);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
long endTime = System.currentTimeMillis();
timeCost = endTime - startTime;
System.out.println("[Thread " + Thread.currentThread() + "] Speech synthesis task completed. Time cost: " + timeCost + " ms, RequestId " + requestId);
}
}
}
@Slf4j
public class SynthesizeTextToSpeechWithCallbackConcurrently {
public static void checkoutEnv(String envName, int defaultSize) {
if (System.getenv(envName) != null) {
System.out.println("[ENV CHECK]: " + envName + " "
+ System.getenv(envName));
} else {
System.out.println("[ENV CHECK]: " + envName
+ " Using Default which is " + defaultSize);
}
}
public static void main(String[] args)
throws InterruptedException, NoApiKeyException {
// 以下はシンガポールリージョンの構成です。{WorkspaceId} を実際のワークスペース ID に置き換えてください。リージョンによって構成は異なります。
Constants.baseWebsocketApiUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference";
// 接続プール環境をチェックします
checkoutEnv("DASHSCOPE_CONNECTION_POOL_SIZE", 32);
checkoutEnv("DASHSCOPE_MAXIMUM_ASYNC_REQUESTS", 32);
checkoutEnv("DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST", 32);
checkoutEnv(CosyvoiceObjectPool.COSYVOICE_OBJECTPOOL_SIZE_ENV, CosyvoiceObjectPool.DEFAULT_OBJECT_POOL_SIZE);
int runTimes = 3;
// SpeechSynthesis オブジェクトのプールを作成します
ExecutorService executorService = Executors.newFixedThreadPool(runTimes);
for (int i = 0; i < runTimes; i++) {
// タスク送信時刻を記録します
LocalDateTime submissionTime = LocalDateTime.now();
executorService.submit(new SynthesizeTaskWithCallback(new String[] {
"床前明月光,", "疑是地上霜,", "举头望明月,", "低头思故乡。"}));
}
// ExecutorService をシャットダウンし、すべてのタスクの完了を待ちます
executorService.shutdown();
executorService.awaitTermination(1, TimeUnit.MINUTES);
System.exit(0);
}
}
推奨構成
以下の構成は、指定された仕様の Alibaba Cloud ECS インスタンス上で Qwen-Audio-TTS/CosyVoice 音声合成サービスのみを実行したテスト結果に基づいています。過剰な同時実行はタスク処理の遅延を増加させる可能性があります。
「単一マシンの同時実行数」とは、Qwen-Audio-TTS/CosyVoice 合成タスクを同時に実行している数を指し、ワーカースレッド数に相当します。
ECS 仕様 (Alibaba Cloud) | 最大単一マシン同時実行数 | オブジェクトプールサイズ | 接続プールサイズ |
|---|---|---|---|
4 vCPU、8 GiB | 100 | 500 | 2000 |
8 vCPU、16 GiB | 150 | 500 | 2000 |
16 vCPU、32 GiB | 200 | 500 | 2000 |
リソース管理とエラー処理
-
タスク成功時: 音声合成タスクが正常に完了した後、GenericObjectPool の returnObject メソッドを呼び出して
SpeechSynthesizerオブジェクトをプールに返却し、再利用可能にします。サンプルコードでは、これは
CosyvoiceObjectPool.getInstance().returnObject(synthesizer)に対応します重要未完了または失敗したタスクの
SpeechSynthesizerオブジェクトは返却しないでください。 -
タスク失敗時: SDK 内部エラーまたはビジネスロジック例外によりタスクが中止された場合、以下の操作を実行します:
- 基盤となる WebSocket 接続をクローズします。
- プール内のオブジェクトを無効化して、再利用されないようにします。
// 現在のコードでは、対応する内容は以下の通りです
// 接続をクローズします
synthesizer.getDuplexApi().close(1000, "bye");
// 例外が発生した場合、オブジェクトプール内の synthesizer を無効化します
CosyvoiceObjectPool.getInstance().invalidateObject(synthesizer);
- サーバー側で TaskFailed エラーが発生した場合、追加の処理は不要です。
ウォームアップと遅延測定
DashScope Java SDK の同時実行遅延とパフォーマンスを評価する際には、まず十分なウォームアップ操作を実行してください。これにより、測定値が初期接続オーバーヘッドではなく、安定状態のパフォーマンスを反映するようになります。
接続再利用メカニズム
DashScope Java SDK は、グローバルシングルトン接続プールを使用して WebSocket 接続を効率的に管理および再利用し、高同時実行ワークロードにおける頻繁な接続作成と破棄のオーバーヘッドを削減します。
このメカニズムの動作方法:
-
オンデマンド作成: SDK は起動時に WebSocket 接続を事前に作成しません。接続は最初の呼び出し時に確立されます。
-
時間制限付き再利用: リクエスト完了後、接続は最大 60 秒間プール内で維持され、再利用されます。
- 60 秒以内に新しいリクエストが到着した場合、既存の接続が再利用され、ハンドシェイクのオーバーヘッドを回避します。
- 接続が 60 秒以上アイドル状態になると、リソースを解放するために自動的にクローズされます。
ウォームアップの重要性
以下のシナリオでは、接続プールに再利用可能なアクティブ接続が存在せず、新しい接続を作成する必要があります:
- アプリケーションが起動直後で、まだ呼び出しが行われていない場合。
- サービスが 60 秒以上アイドル状態だったため、プール内の接続はタイムアウトして閉じられました。
これらのシナリオでは、最初のリクエストが完全な WebSocket ハンドシェイク(TCP ハンドシェイク、TLS ネゴシエーション、プロトコルアップグレード)を完了する必要があるため、後続の接続再利用リクエストよりも大幅に高い遅延が発生します。ウォームアップを行わないと、パフォーマンステストの結果がこの初期接続オーバーヘッドによって歪められます。
SDK 報告遅延 vs 実際の最初のパケット遅延
SDK が報告する最初のパケット遅延(例: get_first_package_delay() の値)には WebSocket 接続設定とネットワーク伝送時間が含まれており、モデルサービスの実際の最初のパケット遅延と等しくありません。
実際の最初のパケット遅延は、サーバーが run-task 命令を受信してから最初の result-generated イベントを返すまでの間隔です。この値はサーバー側ログで確認できます。
高同時実行シナリオでは、同時接続作成とリソーススケジューリングにより、SDK 報告遅延が実際のサーバー側最初のパケット遅延よりも大幅に高くなる可能性があります。SDK 報告遅延が高いと感じられる場合は:
- サーバー側ログの最初のパケット遅延(
run-taskから最初のresult-generatedまで)と比較して、モデル推論パフォーマンスが正常かどうかを確認します。 - 上記で説明したオブジェクトプールまたは接続プールメカニズムを使用してウォームアップを行います。これにより WebSocket ハンドシェイクのオーバーヘッドが排除され、SDK 報告遅延が実際の最初のパケット遅延に近づきます。
推奨プラクティス
信頼性の高いパフォーマンスデータを得るには、負荷テストまたは遅延メトリクス収集を開始する前に、以下のウォームアップ手順に従ってください:
- ターゲット同時実行レベルをシミュレートし、事前に十分なウォームアップリクエストを送信します(例: 1~2 分間トラフィックを維持)。
- 正式なパフォーマンスデータ収集を開始する前に、接続プールが十分なアクティブ接続を確立および維持していることを確認します。
適切なウォームアップにより、SDK 接続プールが安定した再利用状態になり、オンライン安定状態のパフォーマンスを正確に反映した遅延メトリクスが得られます。
一般的な Java SDK 例外
例外 1: 安定したトラフィックにもかかわらず、サーバー TCP 接続が増加し続ける
各 SDK オブジェクトは初期化時に接続を作成します。オブジェクトプールを使用しない場合、各タスク完了後にオブジェクトが破棄されます。その後、接続は参照されない状態になり、サーバーが 61 秒後にタイムアウトするまで開いたままになります。この期間中、接続は再利用できません。
高同時実行下では、再利用可能な接続が見つからない新しいタスクが新しい接続を作成し、以下を引き起こします:
- 接続数が継続的に増加する。
- 過剰な接続によりサーバーリソースが枯渇し、パフォーマンスが低下する。
- 接続プールが飽和し、新しいタスクが利用可能な接続を待機するためにブロックされる。
オブジェクトプールの MaxIdle が MaxTotal よりも低く設定されているため、MaxIdle を超えるアイドルオブジェクトが破棄され、その接続がリークします。これらのリークした接続は、タイプ 1 と同様に、タイムアウトして切断されるまで 61 秒待機する必要があります。
解決策:
タイプ 1: オブジェクトプールを使用します。
タイプ 2: オブジェクトプール構成を確認し、MaxIdle を MaxTotal と等しく設定します。自動オブジェクト退避ポリシーを無効にします。
例外 2: タスクが通常より 60 秒長くなる
例外 1 と同じ根本原因: 接続プールが最大接続制限に達しており、新しいタスクは参照されない接続がタイムアウトするのを 61 秒待つ必要があります。その後、接続が利用可能になります。
例外 3: サービス起動時はタスクが遅いが、徐々に回復する
根本原因:
高同時実行下では、単一のオブジェクトが同じ WebSocket 接続を再利用します。WebSocket 接続はサービス起動時にのみ作成されます。起動直後に高同時実行を開始すると、多くの WebSocket 接続を同時に作成しようとしてブロッキングが発生します。
解決策:
サービス起動後に同時実行を徐々に増加させるか、ウォームアップタスクを追加します。
例外 4: サーバーが "Invalid action('run-task')! Please follow the protocol!" と報告する
根本原因:
クライアント側でエラーが発生しましたが、サーバーはそれを検出しませんでした。接続はタスク中状態のまま維持されています。この接続とオブジェクトが次のタスクで再利用されると、プロトコルフローが破損し、次のタスクが失敗します。
解決策:
例外がスローされた後、オブジェクトをプールに戻す前に WebSocket 接続をクローズします。
例外 5: 安定したビジネス負荷にもかかわらず、トラフィックスパイクが発生する
根本原因:
多くの WebSocket 接続を同時に作成しようとしてブロッキングが発生します。ブロッキング中に、着信ビジネストラフィックが蓄積されます。ブロッキングが解消されると、蓄積されたすべてのタスクが一度に実行され、アカウントの同時実行制限を超える可能性のあるトラフィックスパイクが発生し、タスク失敗やサーバー性能低下を引き起こします。
多くの WebSocket 接続を同時に作成する一般的なトリガー:
- サービス起動フェーズ
- ネットワーク異常により多くの WebSocket 接続が同時に切断・再接続される
- サーバー側エラーの急増により大量の WebSocket 再接続が発生する。一般的なエラーには、アカウント同時実行制限超過 ("Requests rate limit exceeded, please try again later.") が含まれます。
解決策:
- ネットワーク状況を確認します。
- スパイクの前に他のサーバー側エラーの急増が発生していないか調査します。
- アカウント同時実行制限を引き上げます。
- オブジェクトプールおよび接続プールサイズを縮小して、プール制限を通じて最大同時実行数を制限します。
- サーバー仕様をアップグレードするか、マシンを追加します。
例外 6: 同時実行数が増加するにつれてすべてのタスクが遅くなる
解決策:
- ネットワーク帯域幅が上限に達しているかどうかを確認してください。
- 実際の同時実行数が高すぎないか確認します。
サポートされているモデルとリージョン
シンガポール
以下のモデルを呼び出すには、シンガポールリージョンのAPI キーを選択してください:
-
Qwen-Audio-TTS: qwen-audio-3.0-tts-plus、qwen-audio-3.1-tts-flash、qwen-audio-3.0-tts-flash
-
Qwen-Audio-TTS/CosyVoice: cosyvoice-v3-plus、cosyvoice-v3-flash
-
Qwen-TTS:
- Qwen3-TTS-Flash-Realtime: qwen3-tts-flash-realtime (安定版)
中国 (北京)
以下のモデルを呼び出すには、北京リージョンのAPI キーを選択してください:
-
Qwen-Audio-TTS: qwen-audio-3.0-tts-plus、qwen-audio-3.1-tts-flash、qwen-audio-3.0-tts-flash
-
Qwen-Audio-TTS/CosyVoice: cosyvoice-v3.5-plus、cosyvoice-v3.5-flash、cosyvoice-v3-plus、cosyvoice-v3-flash、cosyvoice-v2
-
Qwen-TTS:
- Qwen3-TTS-Flash-Realtime: qwen3-tts-flash-realtime (安定版)
サポートされている音声
モデルによってサポートされている音声は異なります。音声パラメーター列の値を voice リクエストパラメーターに設定してください。
- Qwen-Audio-TTS 音声リスト
- CosyVoice 音声リスト
- Qwen-TTS 音声リスト
API リファレンス
- リアルタイム音声合成 - Qwen-Audio-TTS API リファレンス / リアルタイム音声合成 - CosyVoice API リファレンス
- リアルタイム音声合成 - Qwen-TTS API リファレンス
- AOQ クライアント SDK (CosyVoice モデル用)
よくある質問
Q: 音声合成で発音が正しくない場合や、多音字の発音を制御するにはどうすればよいですか?
- 多音字を同音異義語に置き換えて、発音の問題を迅速に修正します。
- SSML マークアップを使用して発音を制御します 。
Q: クローン音声を使用した際に音声が無音になる場合はどのようにトラブルシューティングすればよいですか?
-
音声ステータスの確認
音声クローニング/デザイン API インターフェイスを呼び出し、音声
statusがOKであることを確認します。 -
モデルバージョンの一貫性の確認
音声クローニング時に使用した
target_modelパラメーターが、音声合成時に使用するmodelパラメーターと一致していることを確認します。例:- クローニングに使用:
cosyvoice-v3-plus - 合成にも使用:
cosyvoice-v3-plus
- クローニングに使用:
-
ソース音声品質の確認
音声クローニングに使用したソース音声が「音声クローニング/デザイン API」の要件を満たしているか確認します:
- 音声の長さ: 10~20 秒
- 明瞭な音声品質
- バックグラウンドノイズなし
-
リクエストパラメーターの確認
音声合成リクエストの
voiceパラメーターが、クローンされた音声 ID に設定されていることを確認します。
Q: クローン音声からの合成音声が不安定または不完全な場合はどうすればよいですか?
クローン音声からの合成音声に以下のいずれかの問題がある場合:
- 音声再生が不完全で、テキストの一部しか話されない
- 合成品質が一貫していない
- 音声に異常なポーズや無音セグメントが含まれている
考えられる原因: ソース音声が品質要件を満たしていない。
解決策: ソース音声が「音声クローニングの録音ガイド」の要件を満たしているか確認します。録音ガイドに従って音声を再録音します。
Q: 合成された音声の実際の長さが WAV ファイルに表示されている長さと異なるのはなぜですか?
音声合成はストリーミングメカニズムを使用し、生成されたデータを随時返します。保存された WAV ファイルヘッダーの長さは推定値であり、不正確な場合があります。正確な長さを取得するには、フォーマットを pcm に設定し、完全な合成結果を待ってから手動で WAV ファイルヘッダーを追加してください。
Q: 音声ファイルが再生できないのはなぜですか?
シナリオに基づいてトラブルシューティングを行います:
-
完全なファイルとして保存された音声 (例: xx.mp3)
- 音声フォーマットの一貫性: リクエストパラメーターの音声フォーマットがファイル拡張子と一致していること (例: パラメーターが wav の場合、ファイルは .wav であること)。
- プレーヤーの互換性: プレーヤーが音声フォーマットとサンプルレートをサポートしていることを確認します。
-
ストリーミング音声再生
- 音声ストリームを完全なファイルとして保存し、メディアプレーヤーで再生を試みます。ファイルが再生できない場合は、上記のシナリオ 1 に従ってください。
- ファイルが正しく再生される場合、問題はストリーミング再生の実装にあります。プレーヤーがストリーミング再生をサポートしていることを確認します (例: ffmpeg、pyaudio、AudioFormat、MediaSource)。
Q: 音声再生がコマ落ちするのはなぜですか?
以下の手順でトラブルシューティングを行います:
-
テキスト送信レートを確認します:次のテキストが到着する前に前の音声セグメントが終了しないよう、送信間隔が適切であることを確認してください。
-
コールバック関数のパフォーマンスの確認:
- コールバック関数内にブロッキングロジックが存在しないことを確認します。
- コールバックは WebSocket スレッドで実行されます。ブロッキング操作はデータ受信に影響を与えます。オーディオデータを別のバッファーに書き込み、別のスレッドで処理します。
-
ネットワークの安定性の確認: ネットワークの変動により、オーディオ伝送が中断または遅延する可能性があります。
Q: 音声合成に時間がかかるのはなぜですか?
以下の手順でトラブルシューティングを行います:
-
入力間隔の確認
ストリーミング合成の場合、テキスト送信間隔が長すぎないか確認します。長い間隔は合成の総時間を増加させます。
-
パフォーマンスメトリクスの分析
- 最初のパケット遅延: 通常は約 500 ms です。
- RTF (リアルタイム係数 = 合成総時間 / 音声長): 1.0 未満であるべきです。
Q: API キーを音声合成サービスのみに制限するには (権限隔離)?
新しいワークスペースを作成し、特定のモデルへのアクセスのみを許可します。これにより、API キーのスコープが制限されます。詳細については、「ワークスペースの管理」をご参照ください。