すべてのプロダクト
Search
ドキュメントセンター

Alibaba Cloud Model Studio:Real-time speech synthesis

最終更新日:Jul 15, 2026

WebSocket を介したストリーミング音声合成は、初回パケットレイテンシーが低いことが特徴です。リアルタイム音声合成は、ストリーミング入力と出力、音声クローニング、音声デザイン、および音声アシスタント、オーディオブック、インテリジェントカスタマーサービス向けのきめ細かな音声制御をサポートします。

概要

低レイテンシーの双方向 WebSocket ストリーミングプロトコルを介して、テキストをリアルタイムで音声に変換します。

  • 初回パケットレイテンシーが低いストリーミング入力と出力

  • 話速、ピッチ、音量、ビットレートを調整可能で、詳細な音声制御が可能

  • 主流の音声フォーマット (PCM、WAV、MP3、Opus) と互換性があり、最大 48 kHz のサンプルレートで出力

  • 命令制御をサポートし、自然言語の命令を通じて音声の表現力を制御できます

  • カスタム音声作成のための音声クローニング音声デザインをサポート

  • 感情およびリッチ言語タグをサポートし、テキスト内に感情や効果音のタグを埋め込むことができます

オーディオブックや教材のナレーションなどのバッチシナリオでは、非リアルタイム音声合成を使用してください。モデル選択のガイダンスについては、「音声合成」をご参照ください。

前提条件

クイックスタート

以下の例は、各モデルの音声合成のデモです。その他の例とパラメーターの詳細については、「API リファレンス」をご参照ください。

Qwen-Audio-TTS

以下の例では、システム音声を使用して音声を合成します。

命令制御機能を使用するには、instruction パラメーターで命令を設定します。

Python

# coding=utf-8

import os
import dashscope
from dashscope.audio.tts_v2 import *

# シンガポールリージョンと北京リージョンでは API キーが異なります。API キーの取得方法:https://www.alibabacloud.com/help/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: longanlingxi などの音声を使用します。
# 各音声は異なる言語をサポートしています。日本語や韓国語など、中国語以外の言語を合成するには、ターゲット言語をサポートする音声を選択してください。詳細は音声リストをご参照ください。
model = "qwen-audio-3.0-tts-flash"
# 音声
voice = "longanlingxi"

# SpeechSynthesizer をインスタンス化し、コンストラクターでモデルや音声などのリクエストパラメーターを渡します
synthesizer = SpeechSynthesizer(model=model, voice=voice)
# 合成するテキストを送信し、バイナリ音声を取得します
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)

Java

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: longanlingxi などの音声を使用します。
    // 各音声は異なる言語をサポートしています。日本語や韓国語など、中国語以外の言語を合成するには、ターゲット言語をサポートする音声を選択してください。詳細は音声リストをご参照ください。
    private static String model = "qwen-audio-3.0-tts-flash";
    // 音声
    private static String voice = "longanlingxi";

    public static void streamAudioDataToSpeaker() {
        // リクエストパラメーター
        SpeechSynthesisParam param =
                SpeechSynthesisParam.builder()
                        // シンガポールリージョンと北京リージョンでは API キーが異なります。API キーの取得方法:https://www.alibabacloud.com/help/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("今日の天気はどうですか?");
        } 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

重要

cosyvoice-v3.5-pluscosyvoice-v3.5-flash は北京リージョンでのみ利用可能で、音声デザインと音声クローニングのシナリオのみをサポートします (システム音声はありません)。使用する前に、音声クローニングまたは音声デザインでカスタム音声を作成し、コード内で voice を音声 ID に、model を対応するモデル名に設定してください。

以下の例では、システム音声を使用して音声を合成します (「CosyVoice 音声リスト」をご参照ください)。

命令制御機能を使用するには、instruction パラメーターで命令を設定します。

Python

# coding=utf-8

import os
import dashscope
from dashscope.audio.tts_v2 import *

# シンガポールリージョンと北京リージョンでは API キーが異なります。API キーの取得方法:https://www.alibabacloud.com/help/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 をインスタンス化し、コンストラクターでモデルや音声などのリクエストパラメーターを渡します
synthesizer = SpeechSynthesizer(model=model, voice=voice)
# 合成するテキストを送信し、バイナリ音声を取得します
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)

Java

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/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("今日の天気はどうですか?");
            } 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-TTS

以下の例では、システム音声を使用して音声を合成します (「サポートされている音声」をご参照ください)。

命令制御機能を使用するには、modelqwen3-tts-instruct-flash-realtime に置き換え、instructions パラメーターで命令を設定します。

Python

Server commit モード

import os
import base64
import threading
import time
import dashscope
from dashscope.audio.qwen_tts_realtime import *


qwen_tts_realtime: QwenTtsRealtime = None
text_to_synthesize = [
    'そうですよね?こういうスーパー大好きなんです。',
    '特に旧正月の時期は、',
    'スーパーに買い物に行きます。',
    'そして、',
    'ものすごくワクワクします!',
    'たくさん買いたくなります!'
]

DO_VIDEO_TEST = False

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/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 キーを手動で設定



class MyCallback(QwenTtsRealtimeCallback):
    def __init__(self):
        self.complete_event = threading.Event()
        self.file = open('result_24k.pcm', 'wb')

    def on_open(self) -> None:
        print('接続が開かれました。プレーヤーを初期化します')

    def on_close(self, close_status_code, close_msg) -> None:
        self.file.close()
        print('接続がコード: {}、メッセージ: {} で閉じられました。プレーヤーを破棄します'.format(close_status_code, close_msg))

    def on_event(self, response: str) -> None:
        try:
            global qwen_tts_realtime
            type = response['type']
            if 'session.created' == type:
                print('セッション開始: {}'.format(response['session']['id']))
            if 'response.audio.delta' == type:
                recv_audio_b64 = response['delta']
                self.file.write(base64.b64decode(recv_audio_b64))
            if 'response.done' == type:
                print(f'レスポンス {qwen_tts_realtime.get_last_response_id()} 完了')
            if 'session.finished' == type:
                print('セッション終了')
                self.complete_event.set()
        except Exception as e:
            print('[エラー] {}'.format(e))
            return

    def wait_for_finished(self):
        self.complete_event.wait()


if __name__  == '__main__':
    init_dashscope_api_key()

    print('初期化中...')

    callback = MyCallback()

    qwen_tts_realtime = QwenTtsRealtime(
        # 命令制御を使用するには、モデルを qwen3-tts-instruct-flash-realtime に置き換えます
        model='qwen3-tts-flash-realtime',
        callback=callback,
        # シンガポールリージョン
        url='wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime'
        )

    qwen_tts_realtime.connect()
    qwen_tts_realtime.update_session(
        voice = 'Cherry',
        response_format = AudioFormat.PCM_24000HZ_MONO_16BIT,
        # 命令制御を使用するには、以下の行のコメントを解除し、モデルを qwen3-tts-instruct-flash-realtime に置き換えます
        # instructions='ファッション製品の紹介に適した、早口で語尾が上がる話し方。',
        # optimize_instructions=True,
        mode = 'server_commit'        
    )
    for text_chunk in text_to_synthesize:
        print(f'テキスト送信: {text_chunk}')
        qwen_tts_realtime.append_text(text_chunk)
        time.sleep(0.1)
    qwen_tts_realtime.finish()
    callback.wait_for_finished()
    print('[メトリック] セッション: {}, 初回音声遅延: {}'.format(
                    qwen_tts_realtime.get_session_id(), 
                    qwen_tts_realtime.get_first_audio_delay(),
                    ))

Commit モード

import base64
import os
import threading
import dashscope
from dashscope.audio.qwen_tts_realtime import *


qwen_tts_realtime: QwenTtsRealtime = None
text_to_synthesize = [
    'これは最初の文です。',
    'これは2番目の文です。',
    'これは3番目の文です。',
]

DO_VIDEO_TEST = False

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/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 キーを手動で設定



class MyCallback(QwenTtsRealtimeCallback):
    def __init__(self):
        super().__init__()
        self.response_counter = 0
        self.complete_event = threading.Event()
        self.file = open(f'result_{self.response_counter}_24k.pcm', 'wb')

    def reset_event(self):
        self.response_counter += 1
        self.file = open(f'result_{self.response_counter}_24k.pcm', 'wb')
        self.complete_event = threading.Event()

    def on_open(self) -> None:
        print('接続が開かれました。プレーヤーを初期化します')

    def on_close(self, close_status_code, close_msg) -> None:
        print('接続がコード: {}、メッセージ: {} で閉じられました。プレーヤーを破棄します'.format(close_status_code, close_msg))

    def on_event(self, response: str) -> None:
        try:
            global qwen_tts_realtime
            type = response['type']
            if 'session.created' == type:
                print('セッション開始: {}'.format(response['session']['id']))
            if 'response.audio.delta' == type:
                recv_audio_b64 = response['delta']
                self.file.write(base64.b64decode(recv_audio_b64))
            if 'response.done' == type:
                print(f'レスポンス {qwen_tts_realtime.get_last_response_id()} 完了')
                self.complete_event.set()
                self.file.close()
            if 'session.finished' == type:
                print('セッション終了')
                self.complete_event.set()
        except Exception as e:
            print('[エラー] {}'.format(e))
            return

    def wait_for_response_done(self):
        self.complete_event.wait()


if __name__  == '__main__':
    init_dashscope_api_key()

    print('初期化中...')

    callback = MyCallback()

    qwen_tts_realtime = QwenTtsRealtime(
        # 命令制御を使用するには、モデルを qwen3-tts-instruct-flash-realtime に置き換えます
        model='qwen3-tts-flash-realtime',
        callback=callback, 
        # シンガポールリージョン
        url='wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime'
        )

    qwen_tts_realtime.connect()
    qwen_tts_realtime.update_session(
        voice = 'Cherry',
        response_format = AudioFormat.PCM_24000HZ_MONO_16BIT,
        # 命令制御を使用するには、以下の行のコメントを解除し、モデルを qwen3-tts-instruct-flash-realtime に置き換えます
        # instructions='ファッション製品の紹介に適した、早口で語尾が上がる話し方。',
        # optimize_instructions=True,
        mode = 'commit'        
    )
    print(f'テキスト送信: {text_to_synthesize[0]}')
    qwen_tts_realtime.append_text(text_to_synthesize[0])
    qwen_tts_realtime.commit()
    callback.wait_for_response_done()
    callback.reset_event()
    
    print(f'テキスト送信: {text_to_synthesize[1]}')
    qwen_tts_realtime.append_text(text_to_synthesize[1])
    qwen_tts_realtime.commit()
    callback.wait_for_response_done()
    callback.reset_event()

    print(f'テキスト送信: {text_to_synthesize[2]}')
    qwen_tts_realtime.append_text(text_to_synthesize[2])
    qwen_tts_realtime.commit()
    callback.wait_for_response_done()
    
    qwen_tts_realtime.finish()
    print('[メトリック] セッション: {}, 初回音声遅延: {}'.format(
                    qwen_tts_realtime.get_session_id(), 
                    qwen_tts_realtime.get_first_audio_delay(),
                    ))

Java

Server commit モード

appendText()

import com.alibaba.dashscope.audio.qwen_tts_realtime.*;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.google.gson.JsonObject;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.AudioSystem;
import java.io.*;
import java.util.Base64;
import java.util.Queue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;

public class Main {
    static String[] textToSynthesize = {
            "そうですよね?こういうスーパー大好きなんです。",
            "特に旧正月の時期は、",
            "スーパーに買い物に行きます。",
            "そして、",
            "ものすごくワクワクします!",
            "たくさん買いたくなります!"
    };
    public static QwenTtsRealtimeAudioFormat ttsFormat = QwenTtsRealtimeAudioFormat.PCM_24000HZ_MONO_16BIT;

    // リアルタイム PCM 音声プレーヤー
    public static class RealtimePcmPlayer {
        private int sampleRate;
        private SourceDataLine line;
        private AudioFormat audioFormat;
        private Thread decoderThread;
        private Thread playerThread;
        private AtomicBoolean stopped = new AtomicBoolean(false);
        private Queue<String> b64AudioBuffer = new ConcurrentLinkedQueue<>();
        private Queue<byte[]> RawAudioBuffer = new ConcurrentLinkedQueue<>();
        private ByteArrayOutputStream totalAudioStream = new ByteArrayOutputStream();

        // オーディオフォーマットとオーディオラインを初期化します。
        public RealtimePcmPlayer(int sampleRate) throws LineUnavailableException {
            this.sampleRate = sampleRate;
            this.audioFormat = new AudioFormat(this.sampleRate, 16, 1, true, false);
            DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
            line = (SourceDataLine) AudioSystem.getLine(info);
            line.open(audioFormat);
            line.start();
            decoderThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (!stopped.get()) {
                        String b64Audio = b64AudioBuffer.poll();
                        if (b64Audio != null) {
                            byte[] rawAudio = Base64.getDecoder().decode(b64Audio);
                            RawAudioBuffer.add(rawAudio);
                            // オーディオデータを totalAudioStream に書き込みます。
                            try {
                                totalAudioStream.write(rawAudio);
                            } catch (IOException e) {
                                throw new RuntimeException(e);
                            }
                        } else {
                            try {
                                Thread.sleep(100);
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                        }
                    }
                }
            });
            playerThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (!stopped.get()) {
                        byte[] rawAudio = RawAudioBuffer.poll();
                        if (rawAudio != null) {
                            try {
                                playChunk(rawAudio);
                            } catch (IOException e) {
                                throw new RuntimeException(e);
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                        } else {
                            try {
                                Thread.sleep(100);
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                        }
                    }
                }
            });
            decoderThread.start();
            playerThread.start();
        }

        // オーディオチャンクを再生し、再生が完了するまでブロックします。
        private void playChunk(byte[] chunk) throws IOException, InterruptedException {
            if (chunk == null || chunk.length == 0) return;

            int bytesWritten = 0;
            while (bytesWritten < chunk.length) {
                bytesWritten += line.write(chunk, bytesWritten, chunk.length - bytesWritten);
            }
            int audioLength = chunk.length / (this.sampleRate*2/1000);
            // バッファリングされたオーディオの再生が完了するのを待ちます。
            Thread.sleep(audioLength - 10);
        }

        public void write(String b64Audio) {
            b64AudioBuffer.add(b64Audio);
        }

        public void cancel() {
            b64AudioBuffer.clear();
            RawAudioBuffer.clear();
        }

        public void waitForComplete() throws InterruptedException {
            while (!b64AudioBuffer.isEmpty() || !RawAudioBuffer.isEmpty()) {
                Thread.sleep(100);
            }
            line.drain();
        }

        public void shutdown() throws InterruptedException, IOException {
            stopped.set(true);
            decoderThread.join();
            playerThread.join();

            // 完全なオーディオファイルを保存します。
            File file = new File("TotalAudio_"+ttsFormat.getSampleRate()+"."+ttsFormat.getFormat());
            try (FileOutputStream fos = new FileOutputStream(file)) {
                fos.write(totalAudioStream.toByteArray());
            }

            if (line != null && line.isRunning()) {
                line.drain();
                line.close();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException, LineUnavailableException, IOException {
        QwenTtsRealtimeParam param = QwenTtsRealtimeParam.builder()
                // 命令制御を使用するには、モデルを qwen3-tts-instruct-flash-realtime に置き換えます。
                .model("qwen3-tts-flash-realtime")
                // 中国 (北京) リージョン
                .url("wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime")
                // API キーはシンガポールと中国 (北京) で異なります。https://www.alibabacloud.com/help/model-studio/get-api-key をご参照ください。
                .apikey(System.getenv("DASHSCOPE_API_KEY"))
                .build();
        AtomicReference<CountDownLatch> completeLatch = new AtomicReference<>(new CountDownLatch(1));
        final AtomicReference<QwenTtsRealtime> qwenTtsRef = new AtomicReference<>(null);

        // リアルタイムオーディオプレーヤーインスタンスを作成します。
        RealtimePcmPlayer audioPlayer = new RealtimePcmPlayer(24000);

        QwenTtsRealtime qwenTtsRealtime = new QwenTtsRealtime(param, new QwenTtsRealtimeCallback() {
            @Override
            public void onOpen() {
                // 接続確立を処理します。
            }
            @Override
            public void onEvent(JsonObject message) {
                String type = message.get("type").getAsString();
                switch(type) {
                    case "session.created":
                        // セッション作成を処理します。
                        if (message.has("session")) {
                            String eventId = message.get("event_id").getAsString();
                            String sessionId = message.get("session").getAsJsonObject().get("id").getAsString();
                            System.out.println("[onEvent] session.created, session_id: "
                                    + sessionId + ", event_id: " + eventId);
                        }
                        break;
                    case "response.audio.delta":
                        String recvAudioB64 = message.get("delta").getAsString();
                        // オーディオをリアルタイムで再生します。
                        audioPlayer.write(recvAudioB64);
                        break;
                    case "response.done":
                        // レスポンス完了を処理します。
                        break;
                    case "session.finished":
                        // セッション終了を処理します。
                        completeLatch.get().countDown();
                    default:
                        break;
                }
            }
            @Override
            public void onClose(int code, String reason) {
                // 接続終了を処理します。
            }
        });
        qwenTtsRef.set(qwenTtsRealtime);
        try {
            qwenTtsRealtime.connect();
        } catch (NoApiKeyException e) {
            throw new RuntimeException(e);
        }
        QwenTtsRealtimeConfig config = QwenTtsRealtimeConfig.builder()
                .voice("Cherry")
                .responseFormat(ttsFormat)
                .mode("server_commit")
                // 命令制御を使用するには、以下の行のコメントを解除し、モデルを qwen3-tts-instruct-flash-realtime に置き換えます。
                // .instructions("")
                // .optimizeInstructions(true)
                .build();
        qwenTtsRealtime.updateSession(config);
        for (String text:textToSynthesize) {
            qwenTtsRealtime.appendText(text);
            Thread.sleep(100);
        }
        qwenTtsRealtime.finish();
        completeLatch.get().await();
        qwenTtsRealtime.close();

        // オーディオ再生が完了するのを待ってから、プレーヤーをシャットダウンします。
        audioPlayer.waitForComplete();
        audioPlayer.shutdown();
        System.exit(0);
    }
}

Commit モード

commit()

import com.alibaba.dashscope.audio.qwen_tts_realtime.*;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.google.gson.JsonObject;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.AudioSystem;
import java.io.*;
import java.util.Base64;
import java.util.Queue;
import java.util.Scanner;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;

public class Main {
    public static QwenTtsRealtimeAudioFormat ttsFormat = QwenTtsRealtimeAudioFormat.PCM_24000HZ_MONO_16BIT;
    // リアルタイム PCM オーディオプレーヤー
    public static class RealtimePcmPlayer {
        private int sampleRate;
        private SourceDataLine line;
        private AudioFormat audioFormat;
        private Thread decoderThread;
        private Thread playerThread;
        private AtomicBoolean stopped = new AtomicBoolean(false);
        private Queue<String> b64AudioBuffer = new ConcurrentLinkedQueue<>();
        private Queue<byte[]> RawAudioBuffer = new ConcurrentLinkedQueue<>();
        private ByteArrayOutputStream totalAudioStream = new ByteArrayOutputStream();


        // オーディオフォーマットとオーディオラインを初期化します。
        public RealtimePcmPlayer(int sampleRate) throws LineUnavailableException {
            this.sampleRate = sampleRate;
            this.audioFormat = new AudioFormat(this.sampleRate, 16, 1, true, false);
            DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
            line = (SourceDataLine) AudioSystem.getLine(info);
            line.open(audioFormat);
            line.start();
            decoderThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (!stopped.get()) {
                        String b64Audio = b64AudioBuffer.poll();
                        if (b64Audio != null) {
                            byte[] rawAudio = Base64.getDecoder().decode(b64Audio);
                            RawAudioBuffer.add(rawAudio);
                            // オーディオデータを totalAudioStream に書き込みます。
                            try {
                                totalAudioStream.write(rawAudio);
                            } catch (IOException e) {
                                throw new RuntimeException(e);
                            }
                        } else {
                            try {
                                Thread.sleep(100);
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                        }
                    }
                }
            });
            playerThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (!stopped.get()) {
                        byte[] rawAudio = RawAudioBuffer.poll();
                        if (rawAudio != null) {
                            try {
                                playChunk(rawAudio);
                            } catch (IOException e) {
                                throw new RuntimeException(e);
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                        } else {
                            try {
                                Thread.sleep(100);
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                        }
                    }
                }
            });
            decoderThread.start();
            playerThread.start();
        }

        // オーディオチャンクを再生し、再生が完了するまでブロックします。
        private void playChunk(byte[] chunk) throws IOException, InterruptedException {
            if (chunk == null || chunk.length == 0) return;

            int bytesWritten = 0;
            while (bytesWritten < chunk.length) {
                bytesWritten += line.write(chunk, bytesWritten, chunk.length - bytesWritten);
            }
            int audioLength = chunk.length / (this.sampleRate*2/1000);
            // バッファリングされたオーディオの再生が完了するのを待ちます。
            Thread.sleep(audioLength - 10);
        }

        public void write(String b64Audio) {
            b64AudioBuffer.add(b64Audio);
        }

        public void cancel() {
            b64AudioBuffer.clear();
            RawAudioBuffer.clear();
        }

        public void waitForComplete() throws InterruptedException {
            // バッファリングされたすべてのオーディオデータの再生が完了するのを待ちます。
            while (!b64AudioBuffer.isEmpty() || !RawAudioBuffer.isEmpty()) {
                Thread.sleep(100);
            }
            // オーディオラインが空になるのを待ちます。
            line.drain();
        }

        public void shutdown() throws InterruptedException {
            stopped.set(true);
            decoderThread.join();
            playerThread.join();
            // 完全なオーディオファイルを保存します。
            File file = new File("TotalAudio_"+ttsFormat.getSampleRate()+"."+ttsFormat.getFormat());
            try (FileOutputStream fos = new FileOutputStream(file)) {
                fos.write(totalAudioStream.toByteArray());
            } catch (FileNotFoundException e) {
                throw new RuntimeException(e);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            if (line != null && line.isRunning()) {
                line.drain();
                line.close();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException, LineUnavailableException, FileNotFoundException {
        Scanner scanner = new Scanner(System.in);

        QwenTtsRealtimeParam param = QwenTtsRealtimeParam.builder()
                // 命令制御を使用するには、モデルを qwen3-tts-instruct-flash-realtime に置き換えます。
                .model("qwen3-tts-flash-realtime")
                // 中国 (北京) リージョン
                .url("wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime")
                // API キーはシンガポールと中国 (北京) で異なります。https://www.alibabacloud.com/help/model-studio/get-api-key をご参照ください。
                .apikey(System.getenv("DASHSCOPE_API_KEY"))
                .build();

        AtomicReference<CountDownLatch> completeLatch = new AtomicReference<>(new CountDownLatch(1));

        // リアルタイムプレーヤーインスタンスを作成します。
        RealtimePcmPlayer audioPlayer = new RealtimePcmPlayer(24000);

        final AtomicReference<QwenTtsRealtime> qwenTtsRef = new AtomicReference<>(null);
        QwenTtsRealtime qwenTtsRealtime = new QwenTtsRealtime(param, new QwenTtsRealtimeCallback() {
            @Override
            public void onOpen() {
                System.out.println("接続が開かれました");
                System.out.println("テキストを入力して Enter キーを押して送信します。「quit」と入力してプログラムを終了します。");
            }
            @Override
            public void onEvent(JsonObject message) {
                String type = message.get("type").getAsString();
                switch(type) {
                    case "session.created":
                        System.out.println("セッション開始: " + message.get("session").getAsJsonObject().get("id").getAsString());
                        break;
                    case "response.audio.delta":
                        String recvAudioB64 = message.get("delta").getAsString();
                        byte[] rawAudio = Base64.getDecoder().decode(recvAudioB64);
                        // オーディオをリアルタイムで再生します。
                        audioPlayer.write(recvAudioB64);
                        break;
                    case "response.done":
                        System.out.println("レスポンス完了");
                        // オーディオ再生が完了するのを待ちます。
                        try {
                            audioPlayer.waitForComplete();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                        // 次の入力に備えます。
                        completeLatch.get().countDown();
                        break;
                    case "session.finished":
                        System.out.println("セッション終了");
                        if (qwenTtsRef.get() != null) {
                            System.out.println("[Metric] response: " + qwenTtsRef.get().getResponseId() +
                                    ", first audio delay: " + qwenTtsRef.get().getFirstAudioDelay() + " ms");
                        }
                        completeLatch.get().countDown();
                    default:
                        break;
                }
            }
            @Override
            public void onClose(int code, String reason) {
                System.out.println("接続終了コード: " + code + ", 理由: " + reason);
                try {
                    // 再生が完了するのを待ってから、プレーヤーをシャットダウンします。
                    audioPlayer.waitForComplete();
                    audioPlayer.shutdown();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        qwenTtsRef.set(qwenTtsRealtime);
        try {
            qwenTtsRealtime.connect();
        } catch (NoApiKeyException e) {
            throw new RuntimeException(e);
        }
        QwenTtsRealtimeConfig config = QwenTtsRealtimeConfig.builder()
                .voice("Cherry")
                .responseFormat(ttsFormat)
                .mode("commit")
                // 命令制御を使用するには、以下の行のコメントを解除し、モデルを qwen3-tts-instruct-flash-realtime に置き換えます。
                // .instructions("")
                // .optimizeInstructions(true)
                .build();
        qwenTtsRealtime.updateSession(config);

        // ループでユーザー入力を読み取ります。
        while (true) {
            System.out.print("合成するテキストを入力してください: ");
            String text = scanner.nextLine();

            // ユーザーが「quit」と入力すると終了します。
            if ("quit".equalsIgnoreCase(text.trim())) {
                System.out.println("接続を閉じています...");
                qwenTtsRealtime.finish();
                completeLatch.get().await();
                break;
            }

            // 空の入力をスキップします。
            if (text.trim().isEmpty()) {
                continue;
            }

            // カウントダウンラッチを再初期化します。
            completeLatch.set(new CountDownLatch(1));

            // テキストを送信します。
            qwenTtsRealtime.appendText(text);
            qwenTtsRealtime.commit();

            // 現在の合成が完了するのを待ちます。
            completeLatch.get().await();
        }

        // リソースをクリーンアップします。
        audioPlayer.waitForComplete();
        audioPlayer.shutdown();
        scanner.close();
        System.exit(0);
    }
}

セッション設定

Qwen-TTS 対話モード

Qwen-TTS Realtime API は、2つの対話モードを提供します:

  • `server_commit` モード:サーバーがテキスト分割と合成タイミングを自動的に処理します。大量のテキストブロックを連続して合成するのに適しています。クライアントは、分割や送信を管理せずにテキストを追加します。

  • `commit` モード:クライアントが明示的にテキストバッファーを送信して合成をトリガーします。会話型 AI のターンごとの合成など、合成タイミングを正確に制御する必要があるシナリオに適しています。

対話モードの切り替え

  • WebSocketsession.update イベントの mode フィールドを設定します。

    {
        "type": "session.update",
        "session": {
            "mode": "server_commit"
        }
    }
  • Python SDKupdate_session メソッドの mode パラメーターを設定します。

    qwen_tts_realtime.update_session(
        voice='Cherry',
        response_format=AudioFormat.PCM_24000HZ_MONO_16BIT,
        mode='server_commit'
    )
  • Java SDKQwenTtsRealtimeConfig.builder() を介して mode パラメーターを設定します。

    QwenTtsRealtimeConfig config = QwenTtsRealtimeConfig.builder()
            .voice("Cherry")
            .responseFormat(ttsFormat)
            .mode("server_commit")
            .build();
    qwenTtsRealtime.updateSession(config);

完全な SDK コード例については、「Python SDK」および「Java SDK」をご参照ください。WebSocket イベントのライフサイクルと接続の再利用の詳細については、「WebSocket API リファレンス」をご参照ください。

高度な機能

命令制御

命令制御は、自然言語の記述を使用して、複雑な音声パラメーターを設定することなく、音声のトーン、速度、感情、音色の特性を調整します。

モデル別の命令仕様

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 文字。中国語の文字 (簡体字/繁体字中国語、日本の漢字、韓国の漢字を含む) はそれぞれ 2 文字としてカウントされます。その他すべての文字 (句読点、アルファベット、数字、日本のかな、韓国のハングルなど) はそれぞれ 1 文字としてカウントされます。

Qwen-TTS

サポートモデル:Qwen3-TTS-Instruct-Flash-Realtime シリーズモデルのみ。

使用方法:instructions パラメーターで命令内容を指定します。

命令テキストでサポートされる言語:中国語と英語のみ。

命令テキストの長さ制限:最大 1,600 トークン。

ユースケース

  • オーディオブックとラジオドラマのナレーション

  • 広告およびプロモーションビデオのナレーション

  • ゲームキャラクターとアニメーションのナレーション

  • 感情表現豊かな音声アシスタント

  • ドキュメンタリーとニュース放送のナレーション

効果的な音声記述の作成

  • 基本原則

    1. 曖昧ではなく、具体的に:「深い」「張りのある」「少し速いペース」など、声質を表す言葉を使用します。「良い」や「普通」などの主観的または曖昧な用語は避けてください。

    2. 一次元的ではなく、多次元的に:良い記述は通常、複数の次元 (性別、年齢、感情など) をカバーします。「女性の声」とだけ書くのは、特徴的な音色を生成するには広すぎます。

    3. 主観的ではなく、客観的に:声の物理的および知覚的な特性に焦点を当てます。例えば、「私のお気に入りの声」ではなく、「エネルギッシュなトーンで高めのピッチ」を使用します。

    4. 模倣ではなく、独創的に:特定の人 (有名人や俳優など) の模倣を要求するのではなく、声質を記述します。モデルは模倣をサポートしておらず、著作権リスクをもたらす可能性があります。

    5. 冗長ではなく、簡潔に:すべての単語が目的を果たすようにします。繰り返しの同義語や無意味な修飾語は避けてください。

  • 記述の次元リファレンス

    以下の次元を組み合わせて声を記述します。含める次元が多いほど、出力はより正確になります。

    次元

    記述例

    性別

    男性、女性、中性的

    年齢

    子供 (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-flashlonganhuan_v3 音声を使用し、命令テキストを "请用河南话表达。" に設定して、河南方言の音声を生成します。

# coding=utf-8

import os
import dashscope
from dashscope.audio.tts_v2 import *

# シンガポールリージョンと北京リージョンでは API キーが異なります。API キーの取得方法:https://www.alibabacloud.com/help/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 をインスタンス化し、コンストラクターでモデルや音声などのリクエストパラメーターを渡します
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 のみ。

制限:単方向ストリーミングモードのみがサポートされています。

制御タグ

制御タグは、音声の感情やスタイルを設定します。テキストにタグを配置すると、次の制御タグが表示されるか、文が長さのために自動的に分割されるまで、後続のすべてのテキストに影響します。

タグ

説明

[sad]

悲しい

[amazed]

驚いた

[deep and loud shouting]

深く大きな叫び声

[trembling]

震える

[angry]

怒っている

[excited]

興奮した

[sarcastic]

皮肉な

[curious]

好奇心旺盛な

[like dracula]

ドラキュラスタイル (深く、不気味)

[bored]

退屈した

[tired]

疲れた

[singing]

歌う

[scornful]

軽蔑的な

[shouting]

叫ぶ

[asmr]

ASMR のようなささやき声

[panicked]

パニックになった

[mischievously]

いたずらっぽい

[empathetic]

共感的な

[whispers]

ささやき

[reluctantly]

しぶしぶ

[crying]

泣いている

[serious]

真面目な

[very slowly]

非常に遅い話し方

[very fast]

非常に速い話し方

リッチ言語タグ

リッチ言語タグは、テキストの現在の位置に声の効果を挿入し、周囲のテキストの感情スタイルには影響しません。

タグ

説明

[gasp]

息をのむ

[sighing]

ため息

[clears throat]

咳払い

[giggles]

くすくす笑う

[laughing]

笑い声

[cough]

[snorts]

Snort

使用例

以下の例は、text パラメーターで制御タグとリッチ言語タグを組み合わせる方法を示しています:

[excited]今日はなんて美しい日なんだ![laughing]一緒に外に出て楽しもう!

このテキストでは、[excited] は後続のすべてのテキストに興奮した感情を適用する制御タグです。[laughing] は、その位置に笑い声を挿入してから残りのテキストの合成を続けるリッチ言語タグです。

同じテキスト内で異なる感情を切り替えることもできます:

[serious]安全上の注意に注意してください。[excited]さあ、今すぐ始めましょう!

ここでは、[serious] が最初の文を真面目なトーンに設定し、[excited] が2番目の文から興奮したトーンに切り替えます。

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/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("接続に失敗しました、HTTP ステータスコード: %d\n", resp.StatusCode)
		}
		fmt.Println("接続に失敗しました:", err)
		return
	}
	defer conn.Close()

	// タスク ID を生成
	taskID := uuid.New().String()
	fmt.Printf("生成されたタスク 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":       "longanlingxi",
				"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("run-task イベントを送信中: %s\n", string(runTaskJSON))

	err = conn.WriteMessage(websocket.TextMessage, runTaskJSON)
	if err != nil {
		fmt.Println("run-task の送信に失敗しました:", err)
		return
	}

	textSent := false

	// メッセージを処理
	for {
		messageType, message, err := conn.ReadMessage()
		if err != nil {
			fmt.Println("メッセージの読み取りに失敗しました:", err)
			break
		}

		// バイナリメッセージを処理
		if messageType == websocket.BinaryMessage {
			fmt.Printf("バイナリメッセージを受信しました、長さ: %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("テキストメッセージを受信しました: %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("イベントタイプ: %s\n", event)

					switch event {
					case "task-started":
						fmt.Println("=== task-started イベントを受信しました ===")

						if !textSent {
							// continue-task イベントを送信

							texts := []string{"ベッドの前、月光が明るく輝き、地面に霜が降りたのかと疑う。", "目を上げて明るい月を見つめ、頭を下げて故郷を思う。"}

							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("continue-task イベントを送信中: %s\n", string(continueTaskJSON))

								err = conn.WriteMessage(websocket.TextMessage, continueTaskJSON)
								if err != nil {
									fmt.Println("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("finish-task イベントを送信中: %s\n", string(finishTaskJSON))

							err = conn.WriteMessage(websocket.TextMessage, finishTaskJSON)
							if err != nil {
								fmt.Println("finish-task の送信に失敗しました:", err)
								return
							}
						}

					case "task-finished":
						fmt.Println("=== タスクが完了しました ===")
						return

					case "task-failed":
						fmt.Println("=== タスクが失敗しました ===")
						if header["error_message"] != nil {
							fmt.Printf("エラーメッセージ: %s\n", header["error_message"])
						}
						return

					case "result-generated":
						fmt.Println("result-generated イベントを受信しました")
					}
				}
			}
		}
	}
}

C#

using System.Net.WebSockets;
using System.Text;
using System.Text.Json;

class Program {
    // シンガポールリージョンと北京リージョンでは API キーが異なります。API キーの取得方法:https://www.alibabacloud.com/help/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 環境変数が設定されていません。");

    // 以下はシンガポールリージョンの設定です。"{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 = {
                "ベッドの前、月光が明るく輝き、",
                "地面に霜が降りたのかと疑う。",
                "目を上げて明るい月を見つめ、",
                "頭を下げて故郷を思う。"
            };
            foreach (string text in texts) {
                await SendContinueTaskCommandAsync(text);
            }

            // finish-task イベントを送信
            await SendFinishTaskCommandAsync(_taskId);

            // 受信タスクが完了するのを待機
            await receiveTask;

            Console.WriteLine("タスクが完了し、接続が閉じられました。");
        } catch (OperationCanceledException) {
            Console.WriteLine("タスクがキャンセルされました。");
        } catch (Exception ex) {
            Console.WriteLine($"エラーが発生しました: {ex.Message}");
        } finally {
            _cancellationTokenSource.Cancel();
            _webSocket.Dispose();
        }
    }

    private static void ClearOutputFile(string filePath) {
        if (File.Exists(filePath)) {
            File.WriteAllText(filePath, string.Empty);
            Console.WriteLine("出力ファイルがクリアされました。");
        } else {
            Console.WriteLine("出力ファイルが存在しないため、クリアする必要はありません。");
        }
    }

    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("WebSocket サービスに正常に接続しました。");
        } catch (OperationCanceledException) {
            Console.WriteLine("WebSocket 接続がキャンセルされました。");
        } catch (Exception ex) {
            Console.WriteLine($"WebSocket 接続に失敗しました: {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 = "longanlingxi",
                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 イベントが送信されました。");
    }

    private static async Task SendContinueTaskCommandAsync(string text) {
        if (_taskId == null) {
            throw new InvalidOperationException("タスク ID が初期化されていません。");
        }

        var command = CreateCommand("continue-task", _taskId, "duplex", new {
            input = new {
                text
            }
        });

        await SendJsonMessageAsync(command);
        Console.WriteLine("continue-task イベントが送信されました。");
    }

    private static async Task SendFinishTaskCommandAsync(string taskId) {
        var command = CreateCommand("finish-task", taskId, "duplex", new {
            input = new { }
        });

        await SendJsonMessageAsync(command);
        Console.WriteLine("finish-task イベントが送信されました。");
    }

    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("メッセージの送信がキャンセルされました。");
        }
    }

    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("タスクが開始されました。");
                        _taskStartedTcs.TrySetResult(true);
                        break;
                    case "task-finished":
                        Console.WriteLine("タスクが完了しました。");
                        _cancellationTokenSource.Cancel();
                        break;
                    case "task-failed":
                        Console.WriteLine("タスクが失敗しました: " + 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("バイナリデータを受信しました...");

                // バイナリデータをファイルに保存
                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("メッセージの受信がキャンセルされました。");
            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/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 "WebSocket サーバーに接続しました\n";

    // タスク ID を生成
    $taskId = generateTaskId();

    // run-task イベントを送信
    sendRunTaskMessage($conn, $taskId);

    // continue-task イベントを送信する関数を定義
    $sendContinueTask = function() use ($conn, $loop, $taskId) {
        // 送信するテキスト
        $texts = ["ベッドの前、月光が明るく輝き、", "地面に霜が降りたのかと疑う。", "目を上げて明るい月を見つめ、", "頭を下げて故郷を思う。"];
        $continueTaskCount = 0;
        foreach ($texts as $text) {
            $continueTaskMessage = json_encode([
                "header" => [
                    "action" => "continue-task",
                    "task_id" => $taskId,
                    "streaming" => "duplex"
                ],
                "payload" => [
                    "input" => [
                        "text" => $text
                    ]
                ]
            ]);
            echo "continue-task イベントを送信中: " . $continueTaskMessage . "\n";
            $conn->send($continueTaskMessage);
            $continueTaskCount++;
        }
        echo "送信された continue-task イベントの数: " . $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 "不明なメッセージ形式\n";
            }
        }
    });

    // 接続クローズをリッスン
    $conn->on('close', function($code = null, $reason = null) {
        echo "接続が閉じられました\n";
        if ($code !== null) {
            echo "クローズコード: " . $code . "\n";
        }
        if ($reason !== null) {
            echo "クローズ理由: " . $reason . "\n";
        }
    });
}, function ($e) {
    echo "接続できません: {$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" => "longanlingxi",
                "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 "run-task イベントを送信中: " . $runTaskMessage . "\n";
    $conn->send($runTaskMessage);
    echo "run-task イベントが送信されました\n";
}

/**
 * オーディオファイルを読み込む
 * @param string $filePath
 * @return bool|string
 */
function readAudioFile(string $filePath) {
    $voiceData = file_get_contents($filePath);
    if ($voiceData === false) {
        echo "オーディオファイルの読み込みに失敗しました\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 "finish-task イベントを送信中: " . $finishTaskMessage . "\n";
    $conn->send($finishTaskMessage);
    echo "finish-task イベントが送信されました\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 "タスクが開始されました、continue-task イベントを送信中...\n";
            $taskStarted = true;
            // continue-task イベントを送信
            $sendContinueTask();
            break;
        case 'result-generated':
            // result-generated イベントを受信
            break;
        case 'task-finished':
            echo "タスクが完了しました\n";
            $conn->close();
            break;
        case 'task-failed':
            echo "タスクが失敗しました\n";
            echo "エラーコード: " . $response['header']['error_code'] . "\n";
            echo "エラーメッセージ: " . $response['header']['error_message'] . "\n";
            $conn->close();
            break;
        case 'error':
            echo "エラー: " . $response['payload']['message'] . "\n";
            break;
        default:
            echo "不明なイベント: " . $response['header']['event'] . "\n";
            break;
    }

    // タスクが完了した場合、接続を閉じる
    if ($response['header']['event'] == 'task-finished') {
        // すべてのデータが送信されたことを確認するために 1 秒待機
        $loop->addTimer(1, function() use ($conn) {
            $conn->close();
            echo "クライアントが接続を閉じています\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/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('WebSocket サーバーに接続しました');

  // 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: 'longanlingxi', // 音声
        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 メッセージが送信されました');
});

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('タスクが開始されました');
        // continue-task イベントを送信
        sendContinueTasks(ws);
        break;
      case 'task-finished':
        console.log('タスクが完了しました');
        ws.close();
        fileStream.end(() => {
          console.log('ファイルストリームが閉じられました');
        });
        break;
      case 'task-failed':
        console.error('タスクが失敗しました: ', message.header.error_message);
        ws.close();
        fileStream.end(() => {
          console.log('ファイルストリームが閉じられました');
        });
        break;
      default:
        // result-generated はここで処理できます
        break;
    }
  }
});

function sendContinueTasks(ws) {
  const texts = [
    'ベッドの前、月光が明るく輝き、',
    '地面に霜が降りたのかと疑う。',
    '目を上げて明るい月を見つめ、',
    '頭を下げて故郷を思う。'
  ];

  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 が送信されました、テキスト: ${text}`);
      }
    }, index * 1000); // 1 秒ごとに 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 が送信されました');
    }
  }, texts.length * 1000 + 1000); // すべての continue-task イベントが送信された 1 秒後に送信
}

ws.on('close', () => {
  console.log('WebSocket サーバーから切断されました');
});

Java

開発には Java DashScope SDK の使用を推奨します。「Java SDK」をご参照ください。

以下は Java WebSocket 直接接続の例です。実行する前にこれらの依存関係をインポートしてください:

  • Java-WebSocket

  • jackson-databind

Maven または Gradle を使用して依存関係を管理します:

pom.xml

<dependencies>
    <!-- WebSocket クライアント -->
    <dependency>
        <groupId>org.java-websocket</groupId>
        <artifactId>Java-WebSocket</artifactId>
        <version>1.5.3</version>
    </dependency>

    <!-- JSON 処理 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.13.0</version>
    </dependency>
</dependencies>

build.gradle

// 他のコードは省略
dependencies {
  // WebSocket クライアント
  implementation 'org.java-websocket:Java-WebSocket:1.5.3'
  // JSON 処理
  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("接続が確立されました");

        // 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\": \"longanlingxi\", \"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("サーバーからのメッセージを受信しました: " + 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("サーバーから task-started イベントを受信しました");

                        List<String> texts = Arrays.asList(
                                "ベッドの前、月光が明るく輝き、地面に霜が降りたのかと疑う。",
                                "目を上げて明るい月を見つめ、頭を下げて故郷を思う。"
                        );

                        for (String text : texts) {
                            // continue-task イベントを送信
                            sendContinueTask(text);
                        }

                        // finish-task イベントを送信
                        sendFinishTask();
                    } else if ("task-finished".equals(event)) {
                        System.out.println("サーバーから task-finished イベントを受信しました");
                        taskFinished = true;
                        closeConnection();
                    } else if ("task-failed".equals(event)) {
                        System.out.println("タスクが失敗しました: " + message);
                        closeConnection();
                    }
                }
            }
        } catch (Exception e) {
            System.err.println("例外が発生しました: " + e.getMessage());
        }
    }

    @Override
    public void onMessage(ByteBuffer message) {
        System.out.println("サイズ " + message.remaining() + " のバイナリ音声データを受信しました");

        try (FileOutputStream fos = new FileOutputStream(outputFile, true)) {
            byte[] buffer = new byte[message.remaining()];
            message.get(buffer);
            fos.write(buffer);
            System.out.println("音声データがローカルファイル " + outputFile + " に書き込まれました");
        } catch (IOException e) {
            System.err.println("ローカルファイルへの音声データの書き込みに失敗しました: " + e.getMessage());
        }
    }

    @Override
    public void onClose(int code, String reason, boolean remote) {
        System.out.println("接続が閉じられました: " + reason + " (" + code + ")");
    }

    @Override
    public void onError(Exception ex) {
        System.err.println("エラー: " + 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/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("DASHSCOPE_API_KEY 環境変数を設定してください");
                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("WebSocket サービスへの接続に失敗しました: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

Python

開発には Python DashScope SDK の使用を推奨します。「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 が接続されました")

        # 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": "longanlingxi",
                    "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("run-task イベントを送信しました")

    def on_message(self, ws, message):
        """
    メッセージが受信されたときのコールバック
    テキストメッセージとバイナリメッセージを異なる方法で処理
    """
        if isinstance(message, str):
            # JSON テキストメッセージを処理
            try:
                msg_json = json.loads(message)
                print(f"JSON メッセージを受信しました: {msg_json}")

                if "header" in msg_json:
                    header = msg_json["header"]

                    if "event" in header:
                        event = header["event"]

                        if event == "task-started":
                            print("タスクが開始されました")
                            self.task_started = True

                            # continue-task イベントを送信
                            texts = [
                                "ベッドの前、月光が明るく輝き、地面に霜が降りたのかと疑う。",
                                "目を上げて明るい月を見つめ、頭を下げて故郷を思う。"
                            ]

                            for text in texts:
                                self.send_continue_task(text)

                            # すべての continue-task イベントが送信された後に finish-task を送信
                            self.send_finish_task()

                        elif event == "task-finished":
                            print("タスクが完了しました")
                            self.task_finished = True
                            self.close(ws)

                        elif event == "task-failed":
                            error_msg = msg_json.get("error_message", "不明なエラー")
                            print(f"タスクが失敗しました: {error_msg}")
                            self.task_finished = True
                            self.close(ws)

            except json.JSONDecodeError as e:
                print(f"JSON の解析に失敗しました: {e}")
        else:
            # バイナリメッセージ (オーディオデータ) を処理
            print(f"バイナリメッセージを受信しました、サイズ: {len(message)} バイト")
            with open(self.output_file, "ab") as f:
                f.write(message)
            print(f"オーディオデータがローカルファイル {self.output_file} に書き込まれました")

    def on_error(self, ws, error):
        """エラーが発生したときのコールバック"""
        print(f"WebSocket エラー: {error}")

    def on_close(self, ws, close_status_code, close_msg):
        """接続が閉じられたときのコールバック"""
        print(f"WebSocket が閉じられました: {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"continue-task イベントを送信しました、テキストコンテンツ: {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("finish-task イベントを送信しました")

    def close(self, ws):
        """接続を能動的に閉じる"""
        if ws and ws.sock and ws.sock.connected:
            ws.close()
            print("接続が能動的に閉じられました")

    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("WebSocket メッセージをリッスン中...")
        self.ws.run_forever()  # 永続的な接続リスナーを開始

# 使用例
if __name__ == "__main__":
    # シンガポールリージョンと北京リージョンでは API キーが異なります。API キーの取得方法:https://www.alibabacloud.com/help/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

  1. クライアントの作成

    Python

    Python ファイル tts_realtime_client.py を作成し、以下のコードをファイルにコピーします:

    # -- 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 Realtime API と対話するためのクライアント。
    
        このクラスは、TTS Realtime API への接続、テキストデータの送信、
        オーディオ出力の受信、および WebSocket 接続の管理のためのメソッドを提供します。
    
        属性:
            base_url (str):
                Realtime API のベース URL。
            api_key (str):
                認証用の API キー。
            voice (str):
                サーバーサイドの音声合成に使用される音声。
            mode (SessionMode):
                セッションモード、server_commit または commit。
            audio_callback (Callable[[bytes], None]):
                オーディオデータを受信するためのコールバック関数。
            language_type(str)
                合成音声の言語。オプション: Chinese, English, German, Italian, Portuguese, Spanish, Japanese, Korean, French, Russian, 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 Realtime 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,
                # 命令制御機能を使用するには、以下の行のコメントを解除し、server_commit.py または commit.py でモデルを qwen3-tts-instruct-flash-realtime に置き換えます
                # "instructions": "ファッション製品の紹介に適した、早口で顕著な上昇イントネーションで話す。",
                # "optimize_instructions": true
                "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"イベント送信: 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("セッション設定を更新中: ", 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"受信イベント: {event_type}")
    
                    if event_type == "error":
                        print("エラー: ", event.get('error', {}))
                        continue
                    elif event_type == "session.created":
                        print("セッション作成、ID: ", event.get('session', {}).get('id'))
                    elif event_type == "session.updated":
                        print("セッション更新、ID: ", event.get('session', {}).get('id'))
                    elif event_type == "input_text_buffer.committed":
                        print("テキストバッファコミット、アイテム ID: ", event.get('item_id'))
                    elif event_type == "input_text_buffer.cleared":
                        print("テキストバッファクリア")
                    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("レスポンス作成、ID: ", self._current_response_id)
                    elif event_type == "response.output_item.added":
                        self._current_item_id = event.get("item", {}).get("id")
                        print("出力アイテム追加、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("オーディオ生成完了")
                    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("レスポンス完了")
                    elif event_type == "session.finished":
                        print("セッション終了")
    
            except websockets.exceptions.ConnectionClosed:
                print("接続が閉じられました")
            except Exception as e:
                print("メッセージ処理エラー: ", str(e))
    
        async def close(self) -> None:
            """WebSocket 接続を閉じます。"""
            if self.ws:
                await self.ws.close()

    Java

    Java ファイル TTSRealtimeClient.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 Realtime API と対話するためのクライアント。
     *
     * このクラスは、TTS Realtime 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 Realtime 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 接続が確立されました");
                    // デフォルトのセッション設定を送信
                    JsonObject session = new JsonObject();
                    session.addProperty("mode", mode.getValue());
                    session.addProperty("voice", TTSRealtimeClient.this.voice);
                    // 命令制御機能を使用するには、以下の行のコメントを解除し、モデルを qwen3-tts-instruct-flash-realtime に置き換えます
                    // session.addProperty("instructions", "ファッション製品の紹介に適した、早口で顕著な上昇イントネーションで話す。");
                    // session.addProperty("optimize_instructions", true);
                    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("受信イベント: " + eventType);
                    }
    
                    switch (eventType) {
                        case "error":
                            System.err.println("エラー: " + event.get("error"));
                            break;
                        case "session.created":
                            System.out.println("セッション作成、ID: " +
                                event.getAsJsonObject("session").get("id").getAsString());
                            break;
                        case "session.updated":
                            System.out.println("セッション更新、ID: " +
                                event.getAsJsonObject("session").get("id").getAsString());
                            break;
                        case "input_text_buffer.committed":
                            System.out.println("テキストバッファコミット、アイテム ID: " + event.get("item_id"));
                            break;
                        case "input_text_buffer.cleared":
                            System.out.println("テキストバッファクリア");
                            break;
                        case "response.created":
                            System.out.println("レスポンス作成、ID: " +
                                event.getAsJsonObject("response").get("id").getAsString());
                            responseDoneLatch = new CountDownLatch(1);
                            break;
                        case "response.output_item.added":
                            System.out.println("出力アイテム追加、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("オーディオ生成完了");
                            break;
                        case "response.done":
                            System.out.println("レスポンス完了");
                            responseDoneLatch.countDown();
                            break;
                        case "session.finished":
                            System.out.println("セッション終了");
                            sessionFinishedLatch.countDown();
                            break;
                    }
                }
    
                @Override
                public void onClose(int code, String reason, boolean remote) {
                    System.out.println("接続が閉じられました: " + reason);
                }
    
                @Override
                public void onError(Exception ex) {
                    System.err.println("WebSocket エラー: " + ex.getMessage());
                }
            };
            ws.connectBlocking();
        }
    
        /**
         * サーバーにイベントを送信します。
         */
        public void sendEvent(JsonObject event) {
            String eventId = "event_" + System.currentTimeMillis();
            event.addProperty("event_id", eventId);
            System.out.println("イベント送信: 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("セッション設定を更新中: " + 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();
            }
        }
    }
  2. 音声合成モードの選択

    Realtime API は 2 つのモードをサポートしています:

    • `server_commit` モード

      サーバーがテキスト分割と合成タイミングを自動的に処理します。クライアントはテキストを送信するだけです。低レイテンシーのシナリオ (GPS ナビゲーションなど) に適しています。

    • `commit` モード

      クライアントはテキストをバッファに追加し、明示的に合成をトリガーします。正確な文分割制御が必要なシナリオ (ニュース放送など) に適しています。

    `server_commit` モード

    Python

    tts_realtime_client.py と同じディレクトリに、もう 1 つの Python ファイル server_commit.py を作成し、以下のコードをファイルにコピーします:

    import os
    import asyncio
    import logging
    import wave
    from tts_realtime_client import TTSRealtimeClient, SessionMode
    import pyaudio
    
    # QwenTTS サービス設定
    # 命令制御機能を使用するには、モデルを qwen3-tts-instruct-flash-realtime に置き換え、tts_realtime_client.py の instructions のコメントを解除します
    # 以下はシンガポールリージョンの設定です。
    URL = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime?model=qwen3-tts-flash-realtime"
    # API キーはシンガポールリージョンと北京リージョンで異なります。API キーの取得方法:https://www.alibabacloud.com/help/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 _produce_text(client: TTSRealtimeClient):
        """サーバーにテキストフラグメントを送信"""
        text_fragments = [
            "Alibaba Cloud の大規模言語モデルプラットフォーム、Model Studio は、大規模言語モデルアプリケーションを開発および構築するためのオールインワンプラットフォームです。",
            "開発者とビジネスユーザーの両方が、大規模言語モデルアプリケーションの設計と開発に深く関与できます。",
            "シンプルなインターフェースを使用して 5 分で大規模言語モデルアプリケーションを開発できます。",
            "または、数時間で専用モデルをトレーニングし、アプリケーションの革新により多くのエネルギーを集中させることができます。",
        ]
    
        logging.info("テキストフラグメントを送信中…")
        for text in text_fragments:
            logging.info(f"フラグメントを送信中: {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("QwenTTS Realtime Client デモを開始中…")
        asyncio.run(_run_demo())
    
    if __name__ == "__main__":
        main()

    server_commit.py を実行すると、Realtime API によって生成されたオーディオをリアルタイムで聞くことができます。

    Java

    TTSRealtimeClient.java と同じディレクトリに、もう 1 つの Java ファイル ServerCommit.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/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("DASHSCOPE_API_KEY 環境変数を設定してください");
            }
    
            // オーディオ再生を初期化
            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 クライアントを作成
            // 命令制御機能を使用するには、モデルを qwen3-tts-instruct-flash-realtime に置き換え、TTSRealtimeClient.java の instructions のコメントを解除します
            TTSRealtimeClient client = new TTSRealtimeClient(
                URL, API_KEY, "Cherry",
                TTSRealtimeClient.SessionMode.SERVER_COMMIT,
                audioData -> {
                    playbackQueue.add(audioData);
                    audioChunks.add(audioData);
                    System.out.println("受信したオーディオデータ: " + audioData.length + " バイト");
                }
            );
    
            client.connect();
    
            // テキストフラグメントを送信
            String[] textFragments = {
                "Alibaba Cloud の大規模言語モデルプラットフォーム、Model Studio は、大規模言語モデルアプリケーションを開発および構築するためのオールインワンプラットフォームです。",
                "開発者とビジネスユーザーの両方が、大規模言語モデルアプリケーションの設計と開発に深く関与できます。",
                "シンプルなインターフェースを使用して 5 分で大規模言語モデルアプリケーションを開発できます。",
                "または、数時間で専用モデルをトレーニングし、アプリケーションの革新により多くのエネルギーを集中させることができます。"
            };
    
            System.out.println("テキストの送信を開始しています...");
            for (String text : textFragments) {
                System.out.println("フラグメントを送信中: " + 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("完了");
        }
    
        private static void saveWav(String filename) throws IOException {
            if (audioChunks.isEmpty()) {
                System.out.println("保存するオーディオデータがありません");
                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("オーディオを保存しました: outputs/" + filename);
        }
    }

    ServerCommit.java をコンパイルして実行すると、Realtime API によって生成されたオーディオをリアルタイムで聞くことができます。

    `commit` モード

    Python

    tts_realtime_client.py と同じディレクトリに、もう 1 つの Python ファイル commit.py を作成し、以下のコードをファイルにコピーします:

    import os
    import asyncio
    import logging
    import wave
    from tts_realtime_client import TTSRealtimeClient, SessionMode
    import pyaudio
    
    # QwenTTS サービス設定
    # 命令制御機能を使用するには、モデルを qwen3-tts-instruct-flash-realtime に置き換え、tts_realtime_client.py の instructions のコメントを解除します
    # 以下はシンガポールリージョンの設定です。
    URL = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime?model=qwen3-tts-flash-realtime"
    # API キーはシンガポールリージョンと北京リージョンで異なります。API キーの取得方法:https://www.alibabacloud.com/help/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  # ユーザー入力が完了するのを待つ
    
        # 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("QwenTTS Realtime Client デモを開始中…")
        asyncio.run(_run_demo())
    
    if __name__ == "__main__":
        main()

    commit.py を実行します。テキストを複数回入力して合成できます。テキストを入力せずに Enter を押すと、スピーカーから Realtime API が返したオーディオが聞こえます。

    Java

    TTSRealtimeClient.java と同じディレクトリに、もう 1 つの Java ファイル Commit.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/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("DASHSCOPE_API_KEY 環境変数を設定してください");
            }
    
            // オーディオ再生を初期化
            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 クライアントを作成 (commit モード)
            // 命令制御機能を使用するには、モデルを qwen3-tts-instruct-flash-realtime に置き換え、TTSRealtimeClient.java の instructions のコメントを解除します
            TTSRealtimeClient client = new TTSRealtimeClient(
                URL, API_KEY, "Cherry",
                TTSRealtimeClient.SessionMode.COMMIT,
                audioData -> {
                    playbackQueue.add(audioData);
                    audioChunks.add(audioData);
                    System.out.println("受信したオーディオデータ: " + audioData.length + " バイト");
                }
            );
    
            client.connect();
    
            // 対話型入力
            System.out.println("テキストを入力してください (Enter を直接押すとコミットイベントを送信してセッションを終了し、Ctrl+D を押すとプログラムを終了します):");
            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("空の入力、コミットイベントを送信してセッションを終了します");
                    client.commitTextBuffer();
                    Thread.sleep(300);
                    client.finishSession();
                    break;
                } else {
                    System.out.println("テキストを送信中: " + 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("完了");
        }
    
        private static void saveWav(String filename) throws IOException {
            if (audioChunks.isEmpty()) {
                System.out.println("保存するオーディオデータがありません");
                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("オーディオを保存しました: outputs/" + filename);
        }
    }

    Commit.java をコンパイルして実行します。テキストを複数回入力して合成できます。テキストを入力せずに Enter を押すと、スピーカーから Realtime API が返したオーディオが聞こえます。

本番環境での適用

接続の再利用 (WebSocket)

WebSocket 接続は再利用可能です:合成タスクが完了した後、再確立することなく同じ接続で次のタスクを開始できます。

再利用プロセス

  • Qwen-Audio-TTS / Qwen-Audio-TTS/CosyVoice:クライアントが finish-task を送信し、サーバーが task-finished を返した後、クライアントは run-task を送信して新しいタスクを開始できます。

  • Qwen-TTS:クライアントが session.finish を送信し、サーバーが session.finished を返した後、クライアントは新しいセッションを作成して次のタスクを開始できます。

重要
  1. 新しいタスクを開始する前に、サーバーが完了イベント (task-finished または session.finished) を返すのを待ちます。

  2. Qwen-Audio-TTS, Qwen-Audio-TTS/CosyVoice は、再利用された接続上の各タスクに異なる task_id を必要とします。

  3. タスクが失敗した場合、サーバーはエラーイベントを返し、接続を閉じます。接続は再利用できません。

  4. 前のタスクが終了してから 60 秒以内に新しいタスクが開始されない場合、接続は自動的に閉じられます。

各モデルのイベントの詳細については、対応するAPI リファレンスをご参照ください。

高い同時実行性のベストプラクティス

DashScope SDK には、WebSocket 接続とシンセサイザーオブジェクトを再利用する組み込みのプーリングがあり、それらを繰り返し作成および破棄するオーバーヘッドを排除します。

高い同時実行性のベストプラクティスを表示

Qwen-Audio-TTS/CosyVoice

Qwen-Audio-TTS と Qwen-Audio-TTS/CosyVoice は同じ SDK インターフェイスを使用します。以下の例は Qwen-Audio-TTS モデルにも適用されます — modelvoice パラメーターを置き換えるだけです。

前提条件

Python SDK

Python SDK は SpeechSynthesizerObjectPool を使用して SpeechSynthesizer オブジェクトを管理および再利用します。

プールは、初期化時に指定された数の SpeechSynthesizer インスタンスを作成し、WebSocket 接続を確立します。オブジェクトを借りると、すぐにリクエストを送信する準備ができており、初回パケットレイテンシーを削減します。オブジェクトが返された後、接続は次のタスクのためにアクティブなままです。

実装手順

  1. 依存関係のインストール:DashScope の依存関係をインストールします (pip install -U dashscope)。

  2. オブジェクトプールの作成と設定

    プールサイズをピーク同時実行数の 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 インスタンスを維持します。

  3. プールから SpeechSynthesizer オブジェクトを借用する

    返却されていないオブジェクトの数がプール容量を超えると、システムは追加のオブジェクトを作成します。

    これらの追加オブジェクトは新しい接続を確立する必要があり、プーリングの恩恵を受けません。

    speech_synthesizer = connectionPool.borrow_synthesizer(
        model='cosyvoice-v3-flash',
        voice='longanyang',
        seed=12382,
        callback=synthesizer_callback
    )
  4. 音声合成の実行

    SpeechSynthesizer オブジェクトの call または streaming_call メソッドを呼び出して音声を合成します。

  5. 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 = [
    '文 1: Alibaba 音声合成サービスへようこそ。',
    '文 2: Alibaba 音声合成サービスへようこそ。',
    '文 3: Alibaba 音声合成サービスへようこそ。',
]
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/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}] 開始')

        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}] 終了')

        def on_event(self, message):
            # print(f'音声合成メッセージを受信 {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 関数
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('接続プールを作成中')
        start_time = time.time() * 1000
        connectionPool = SpeechSynthesizerObjectPool(max_size=3)
        end_time = time.time() * 1000
        print('接続プールが作成されました、コスト: {} 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 オブジェクトのセットを維持します。プールから借用することで、接続設定のレイテンシーがなくなり、初回パケット遅延が大幅に削減されます。

実装手順

  1. 依存関係の追加

    使用するビルドツールに基づいて、プロジェクトの依存関係設定に dashscope-sdk-java と commons-pool2 を追加します。

    Maven と Gradle の例:

    Maven

    1. Maven プロジェクトの pom.xml ファイルを開きます。

    2. <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>
    1. pom.xml ファイルを保存します。

    2. mvn clean installmvn compile などの Maven コマンドを実行して、プロジェクトの依存関係を更新します。

    Gradle

    1. Gradle プロジェクトの build.gradle ファイルを開きます。

    2. 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'
      }
    3. build.gradle ファイルを保存します。

    4. ターミナルで、プロジェクトのルートディレクトリに移動し、以下の Gradle コマンドを実行して依存関係を更新します。

      ./gradlew build --refresh-dependencies

      Windows では、代わりに以下のコマンドを使用します:

      gradlew build --refresh-dependencies
  2. 接続プールの設定

    環境変数を通じて主要な接続プールパラメーターを設定します:

    環境変数

    説明

    DASHSCOPE_CONNECTION_POOL_SIZE

    接続プールサイズ。

    推奨値:ピーク同時実行数の少なくとも 2 倍。

    デフォルト:32。

    DASHSCOPE_MAXIMUM_ASYNC_REQUESTS

    非同期リクエストの最大数。

    推奨値:DASHSCOPE_CONNECTION_POOL_SIZE と同じ。

    デフォルト:32。

    DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST

    ホストごとの非同期リクエストの最大数。

    推奨値:DASHSCOPE_CONNECTION_POOL_SIZE と同じ。

    デフォルト:32。

  3. オブジェクトプールの設定

    環境変数を通じてオブジェクトプールのサイズを設定します:

    環境変数

    説明

    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;
        }
    }
  4. プールから SpeechSynthesizer オブジェクトを借用する

    返却されていないオブジェクトの数がプールの最大容量を超えると、システムは追加の SpeechSynthesizer オブジェクトを作成します。

    これらの新しく作成されたオブジェクトは、再初期化と新しい WebSocket 接続が必要なため、プーリングの恩恵を受けません。

    synthesizer = CosyvoiceObjectPool.getInstance().borrowObject();
  5. 音声合成の実行

    プールから SpeechSynthesizer オブジェクトを借用した後、updateParamAndCallback(param, callback) を呼び出して現在のタスクのパラメーターとコールバックをバインドし、次に streamingCall または call を呼び出して音声を合成します。

    重要
    • オブジェクトプールシナリオでは、updateParamAndCallback複数回呼び出されます (オブジェクトが借用されるたびに 1 回、コールバックと voiceformat などのタスクレベルのパラメーターを設定するため)。各呼び出しで渡される apiKey は同じでなければなりません。 updateParamAndCallback は、現在の SpeechSynthesizer インスタンスのローカルフィールドのみを更新し、基盤となる WebSocket 接続を再構築しません。SDK は、認証のために WebSocket ハンドシェイク中にのみ apiKeyAuthorization ヘッダーに書き込みます。後続のタスクメッセージ (例:run-task) は apiKey を携帯しません。再利用された接続が切断されない限り、新しい apiKey を渡してもサーバーには送信されません — リクエストは依然として初期ハンドシェイクの apiKey を使用し、ID、クォータ、または課金の帰属が期待と異なる可能性があります。

    • 複数の API キーを使用するには、キーごとに別のオブジェクトプールインスタンスを維持します。

  6. 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("環境変数のオブジェクトプールサイズを使用: "+ n);
            return n;
        } catch (NumberFormatException e) {
            System.out.println("デフォルトのオブジェクトプールサイズを使用: "+ 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("オーディオを受信しました、オーディオストリームの長さ: " + 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/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("例外 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.currentThread() + "] 音声合成タスクが完了しました。所要時間: " + 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
                    + " デフォルトを使用: " + 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 内部エラーまたはビジネスロジック例外によりタスクが中止された場合、以下を実行します:

    1. 基盤となる WebSocket 接続を閉じます。

    2. 再利用を防ぐために、プール内のオブジェクトを無効にします。

    // 現在のコードでは、対応する内容は以下の通りです
    // 接続を閉じる
    synthesizer.getDuplexApi().close(1000, "bye");
    // 例外発生時にオブジェクトプール内のシンセサイザーを無効にする
    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. ターゲットの同時実行レベルをシミュレートし、事前に多数のウォームアップリクエストを送信して (例:1〜2 分間トラフィックを維持)、接続プールを完全に満たします。

  2. 正式なパフォーマンスデータ収集を開始する前に、接続プールが十分なアクティブな接続を確立し、維持していることを確認します。

適切なウォームアップにより、SDK 接続プールは安定した再利用状態になり、オンラインの定常状態のパフォーマンスを正確に反映するレイテンシーメトリクスが生成されます。

一般的な Java SDK の例外

例外 1:安定したトラフィックにもかかわらず、サーバーの TCP 接続が増え続ける

根本原因:

タイプ 1:

各 SDK オブジェクトは初期化時に接続を作成します。オブジェクトプールがない場合、オブジェクトは各タスク完了後に破棄されます。接続はその後、参照されない状態になり、サーバーが 61 秒後にタイムアウトするまで開いたままになります。この期間中、接続は再利用できません。

高同時実行下では、再利用可能な接続が見つからない新しいタスクは新しい接続を作成し、以下につながります:

  1. 継続的に増加する接続数。

  2. 過剰な接続によるサーバーリソースの枯渇とパフォーマンスの低下。

  3. 接続プールの飽和、新しいタスクが利用可能な接続を待ってブロックされる。

タイプ 2:

オブジェクトプールの 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.」) が含まれます。

解決策

  1. ネットワークの状態を確認します。

  2. スパイクの前に他のサーバー側エラーのバーストがあったかどうかを調査します。

  3. アカウントの同時実行制限を増やします。

  4. オブジェクトプールと接続プールのサイズを減らして、プール制限を通じて最大同時実行数を制限します。

  5. サーバーの仕様をアップグレードするか、マシンを追加します。

例外 6:同時実行数が増加するにつれて、すべてのタスクが遅くなる

解決策

  1. ネットワーク帯域幅が制限に達しているかどうかを確認します。

  2. 実際の同時実行数が高すぎるかどうかを確認します。

サポートされているモデルとリージョン

シンガポール

以下のモデルを呼び出すには、シンガポールリージョンから API キーを選択してください:

  • Qwen-Audio-TTS: qwen-audio-3.0-tts-plus, qwen-audio-3.0-tts-flash

  • Qwen-Audio-TTS/CosyVoice: cosyvoice-v3-plus, cosyvoice-v3-flash

  • Qwen-TTS:

    • Qwen3-TTS-Instruct-Flash-Realtime: qwen3-tts-instruct-flash-realtime (安定版、現在 qwen3-tts-instruct-flash-realtime-2026-01-22 と同等)、qwen3-tts-instruct-flash-realtime-2026-01-22 (最新スナップショット)

    • Qwen3-TTS-VD-Realtime: qwen3-tts-vd-realtime-2026-01-15 (最新スナップショット)、qwen3-tts-vd-realtime-2025-12-16 (スナップショット)

    • Qwen3-TTS-VC-Realtime: qwen3-tts-vc-realtime-2026-01-15 (最新スナップショット)、qwen3-tts-vc-realtime-2025-11-27 (スナップショット)

    • Qwen3-TTS-Flash-Realtime: qwen3-tts-flash-realtime (安定版、現在 qwen3-tts-flash-realtime-2025-11-27 と同等)、qwen3-tts-flash-realtime-2025-11-27 (最新スナップショット)、qwen3-tts-flash-realtime-2025-09-18 (スナップショット)

中国 (北京)

以下のモデルを呼び出すには、北京リージョンから API キーを選択してください:

  • Qwen-Audio-TTS: qwen-audio-3.0-tts-plus, 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-Instruct-Flash-Realtime: qwen3-tts-instruct-flash-realtime (安定版、現在 qwen3-tts-instruct-flash-realtime-2026-01-22 と同等)、qwen3-tts-instruct-flash-realtime-2026-01-22 (最新スナップショット)

    • Qwen3-TTS-VD-Realtime: qwen3-tts-vd-realtime-2026-01-15 (最新スナップショット)、qwen3-tts-vd-realtime-2025-12-16 (スナップショット)

    • Qwen3-TTS-VC-Realtime: qwen3-tts-vc-realtime-2026-01-15 (最新スナップショット)、qwen3-tts-vc-realtime-2025-11-27 (スナップショット)

    • Qwen3-TTS-Flash-Realtime: qwen3-tts-flash-realtime (安定版、現在 qwen3-tts-flash-realtime-2025-11-27 と同等)、qwen3-tts-flash-realtime-2025-11-27 (最新スナップショット)、qwen3-tts-flash-realtime-2025-09-18 (スナップショット)

    • Qwen-TTS-Realtime: qwen-tts-realtime (安定版、現在 qwen-tts-realtime-2025-07-15 と同等)、qwen-tts-realtime-latest (最新版、現在 qwen-tts-realtime-2025-07-15 と同等)、qwen-tts-realtime-2025-07-15 (スナップショット)

サポートされている音声

モデルごとにサポートされている音声が異なります。voice リクエストパラメーターを、対応する音声リストの音声パラメーター列の値に設定してください。

API リファレンス

よくある質問

Q:音声合成で誤った発音を修正するにはどうすればよいですか?多音字の発音を制御するにはどうすればよいですか?

  • 多音字を同音異義語に置き換えて、発音の問題を迅速に修正します。

  • SSML マークアップを使用して発音を制御します。

Q:クローン音声を使用すると音声がサイレントになる問題を解決するにはどうすればよいですか?

  1. 音声ステータスの確認

    音声クローニング/デザイン API インターフェイスを呼び出し、音声の statusOK であることを確認します。

  2. モデルバージョンの一貫性の確認

    音声クローニング時に使用した target_model パラメーターが、音声合成に使用した model パラメーターと一致していることを確認します。例:

    • クローニングに cosyvoice-v3-plus を使用した場合

    • 合成にも cosyvoice-v3-plus を使用する必要があります

  3. ソースオーディオの品質の確認

    音声クローニングに使用したソースオーディオが、「音声クローニング/デザイン API」の要件を満たしているかどうかを確認します:

    • オーディオの持続時間:10〜20 秒

    • クリアな音質

    • バックグラウンドノイズなし

  4. リクエストパラメーターの確認

    音声合成リクエストの voice パラメーターが、クローンされた音声 ID に設定されていることを確認します。

Q:クローン音声から合成されたオーディオが不安定または不完全な場合はどうすればよいですか?

クローン音声から合成されたオーディオに以下のいずれかの問題がある場合:

  • オーディオの再生が不完全で、テキストの一部しか話されない

  • 合成品質が一定しない

  • オーディオに異常な間や無音部分が含まれる

考えられる原因:ソースオーディオが品質要件を満たしていません。

解決策:ソースオーディオが「音声クローニングのための録音ガイド」の要件を満たしているかどうかを確認します。録音ガイドラインに従ってオーディオを再録音してください。

Q:合成されたオーディオの実際の持続時間が、WAV ファイルに表示される持続時間と異なるのはなぜですか?

音声合成は、生成されたデータを返すストリーミングメカニズムを使用します。保存された WAV ファイルヘッダーの持続時間は推定値であり、不正確な場合があります。正確な持続時間を得るには、フォーマットを pcm に設定し、完全な合成結果を待ってから、WAV ファイルヘッダーを手動で追加します。

Q:オーディオファイルが再生できないのはなぜですか?

シナリオに基づいてトラブルシューティングします:

  1. 完全なファイルとして保存されたオーディオ (xx.mp3 など)

    1. オーディオフォーマットの一貫性:リクエストパラメーターのオーディオフォーマットは、ファイル拡張子と一致する必要があります (例:パラメーターが wav の場合、ファイルは .wav である必要があります)。

    2. プレーヤーの互換性:プレーヤーがオーディオフォーマットとサンプルレートをサポートしていることを確認します。

  2. ストリーミングオーディオ再生

    1. オーディオストリームを完全なファイルとして保存し、メディアプレーヤーで再生してみてください。ファイルが再生されない場合は、上記のシナリオ 1 をご参照ください。

    2. ファイルが正しく再生される場合は、ストリーミング再生の実装に問題があります。プレーヤーがストリーミング再生をサポートしていることを確認します (ffmpeg、pyaudio、AudioFormat、MediaSource など)。

Q:オーディオの再生が途切れるのはなぜですか?

以下の手順でトラブルシューティングします:

  1. テキスト送信レートの確認:前のオーディオセグメントが次のテキストが到着する前に終了しないように、送信間隔が合理的であることを確認します。

  2. コールバック関数のパフォーマンスの確認:

    • コールバック関数にブロッキングロジックが存在しないことを確認します。

    • コールバックは WebSocket スレッドで実行されます。ブロッキング操作はデータ受信に影響します。オーディオデータを別のバッファに書き込み、別のスレッドで処理します。

  3. ネットワークの安定性の確認:ネットワークの変動により、オーディオ伝送の中断や遅延が発生する可能性があります。

Q:音声合成に時間がかかるのはなぜですか?

以下の手順でトラブルシューティングします:

  1. 入力間隔の確認

    ストリーミング合成の場合、テキストの送信間隔が長すぎないか確認します。間隔が長いと、合計合成時間が増加します。

  2. パフォーマンスメトリクスの分析

    • 初回パケットレイテンシー:通常は約 500 ms です。

    • RTF (リアルタイム係数 = 合計合成時間 / オーディオ持続時間):1.0 未満である必要があります。

Q:API キーを音声合成サービスのみに制限するにはどうすればよいですか (権限隔離)?

新しいワークスペースを作成し、特定のモデルのみへのアクセスを許可します。これにより、API キーの範囲が制限されます。詳細については、「ワークスペースの管理」をご参照ください。