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

Alibaba Cloud Model Studio:リアルタイム音声合成

最終更新日:Jun 26, 2026

リアルタイム音声合成は、WebSocket 接続を介してテキストを自然な音声に変換します。ストリーミング入出力、音声クローニング、音声デザイン、詳細な音声制御をサポートし、音声アシスタント、オーディオブック、インテリジェントカスタマーサービスなどのユースケースに利用できます。

概要

WebSocket を介した低レイテンシーのリアルタイム音声合成は、音声アシスタント、インテリジェントカスタマーサービス、ライブキャプションなど、即時応答が求められるシナリオ向けに構築されています。

  • ストリーミング入出力 (全二重 WebSocket) と初回音声受信までの時間 (TTFA) の短縮により、音声アシスタントやインテリジェントカスタマーサービスなどのリアルタイムな会話に最適です。

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

  • 主要な音声フォーマット (PCM、WAV、MP3、Opus) と互換性があり、最大 48 kHz のサンプルレート出力をサポートします。

  • 命令ベースの制御をサポートしており、自然言語による命令で音声の表現力を制御できます。

  • 音声クローニングおよび音声デザインによる音声カスタマイズをサポートします。

リアルタイム出力が不要な場合は、オーディオブックや教材の吹き替えなどのバッチシナリオに適した非リアルタイム音声合成 (HTTP API) を使用してください。モデル選択のガイダンスについては、「音声合成」をご参照ください。

前提条件

クイックスタート

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

CosyVoice

重要

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

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

命令ベースの制御を使用するには、instructions パラメーターを通じて命令を設定します。

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')

    # シンガポールリージョンの URL。WorkspaceId を実際のワークスペース ID に置き換えてください。URL はリージョンによって異なります。
    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 などの音声を使用します。
    # 各音声は異なる言語をサポートしています。日本語や韓国語など、中国語以外の言語を合成する場合は、ターゲット言語をサポートする音声を選択してください。詳細については、CosyVoice 音声リストをご参照ください。
    model = "cosyvoice-v3-flash"
    # 音声
    voice = "longanyang"

    # SpeechSynthesizer をインスタンス化し、コンストラクターでモデルや音声などのリクエストパラメーターを渡します
    synthesizer = SpeechSynthesizer(model=model, voice=voice)
    # 合成するテキストを送信し、バイナリ音声を取得します
    audio = synthesizer.call("How is the weather today?")
    # 最初のテキスト送信では WebSocket 接続を確立する必要があるため、初回パケットレイテンシーには接続確立時間が含まれます
    print('[Metric] requestId: {}, first package delay: {} 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 などの音声を使用します。
        // 各音声は異なる言語をサポートしています。日本語や韓国語など、中国語以外の言語を合成する場合は、ターゲット言語をサポートする音声を選択してください。詳細については、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("How is the weather today?");
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                // タスク完了後に WebSocket 接続を閉じます
                synthesizer.getDuplexApi().close(1000, "bye");
            }
            if (audio != null) {
                // 音声データをローカルファイル "output.mp3" に保存します
                File file = new File("output.mp3");
                // 最初のテキスト送信では WebSocket 接続を確立する必要があるため、初回パケットレイテンシーには接続確立時間が含まれます
                // 注意:getFirstPackageDelay() は dashscope-sdk-java 2.18.0 以降が必要です
                System.out.println(
                        "[Metric] requestId: "
                                + synthesizer.getLastRequestId()
                                + ", first package delay (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) {
            // シンガポールリージョンの URL。WorkspaceId を実際のワークスペース ID に置き換えてください。URL はリージョンによって異なります。
            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 = [
    'Right? I love supermarkets like this.',
    'Especially during Chinese New Year,',
    'I go shopping at supermarkets.',
    'And I feel',
    'absolutely thrilled!',
    'I want to buy so many things!'
]

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('connection opened, init player')

    def on_close(self, close_status_code, close_msg) -> None:
        self.file.close()
        print('connection closed with code: {}, msg: {}, destroy player'.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('start session: {}'.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'response {qwen_tts_realtime.get_last_response_id()} done')
            if 'session.finished' == type:
                print('session finished')
                self.complete_event.set()
        except Exception as e:
            print('[Error] {}'.format(e))
            return

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


if __name__  == '__main__':
    init_dashscope_api_key()

    print('Initializing ...')

    callback = MyCallback()

    qwen_tts_realtime = QwenTtsRealtime(
        # 命令ベースの制御を使用するには、モデルを qwen3-tts-instruct-flash-realtime に置き換えます
        model='qwen3-tts-flash-realtime',
        callback=callback,
        # これはシンガポールリージョンの URL です。WorkspaceId を実際のワークスペース ID に置き換えてください。北京リージョンを使用する場合は、wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/realtime に置き換えてください
        url='wss://{WorkspaceId}.ap-southeast-1.maas.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='Speak quickly with a rising intonation, suitable for introducing fashion products.',
        # optimize_instructions=True,
        mode = 'server_commit'        
    )
    for text_chunk in text_to_synthesize:
        print(f'send text: {text_chunk}')
        qwen_tts_realtime.append_text(text_chunk)
        time.sleep(0.1)
    qwen_tts_realtime.finish()
    callback.wait_for_finished()
    print('[Metric] session: {}, first audio delay: {}'.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 = [
    'This is the first sentence.',
    'This is the second sentence.',
    'This is the third sentence.',
]

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('connection opened, init player')

    def on_close(self, close_status_code, close_msg) -> None:
        print('connection closed with code: {}, msg: {}, destroy player'.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('start session: {}'.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'response {qwen_tts_realtime.get_last_response_id()} done')
                self.complete_event.set()
                self.file.close()
            if 'session.finished' == type:
                print('session finished')
                self.complete_event.set()
        except Exception as e:
            print('[Error] {}'.format(e))
            return

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


if __name__  == '__main__':
    init_dashscope_api_key()

    print('Initializing ...')

    callback = MyCallback()

    qwen_tts_realtime = QwenTtsRealtime(
        # 命令ベースの制御を使用するには、モデルを qwen3-tts-instruct-flash-realtime に置き換えます
        model='qwen3-tts-flash-realtime',
        callback=callback, 
        # これはシンガポールリージョンの URL です。WorkspaceId を実際のワークスペース ID に置き換えてください。北京リージョンを使用する場合は、wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/realtime に置き換えてください
        url='wss://{WorkspaceId}.ap-southeast-1.maas.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='Speak quickly with a rising intonation, suitable for introducing fashion products.',
        # optimize_instructions=True,
        mode = 'commit'        
    )
    print(f'send text: {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'send text: {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'send text: {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('[Metric] session: {}, first audio delay: {}'.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 = {
            "Right? I really love this kind of supermarket.",
            "Especially during the Chinese New Year.",
            "Going to the supermarket.",
            "It just makes me feel.",
            "Super, super happy!",
            "I want to buy so many things!"
    };
    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")
                // シンガポールのエンドポイント。WorkspaceId を実際のワークスペース ID に置き換えてください。中国 (北京) の場合、wss://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api-ws/v1/realtime を使用します。
                .url("wss://{WorkspaceId}.ap-southeast-1.maas.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")
                // シンガポールのエンドポイント。WorkspaceId を実際のワークスペース ID に置き換えてください。中国 (北京) の場合、wss://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api-ws/v1/realtime を使用します。
                .url("wss://{WorkspaceId}.ap-southeast-1.maas.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("connection opened");
                System.out.println("Enter text and press Enter to send. Enter 'quit' to exit the program.");
            }
            @Override
            public void onEvent(JsonObject message) {
                String type = message.get("type").getAsString();
                switch(type) {
                    case "session.created":
                        System.out.println("start session: " + 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("response done");
                        // 音声再生が完了するのを待ちます。
                        try {
                            audioPlayer.waitForComplete();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                        // 次の入力に備えます。
                        completeLatch.get().countDown();
                        break;
                    case "session.finished":
                        System.out.println("session finished");
                        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("connection closed code: " + code + ", reason: " + 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("Enter the text to synthesize: ");
            String text = scanner.nextLine();

            // ユーザーが 'quit' を入力すると終了します。
            if ("quit".equalsIgnoreCase(text.trim())) {
                System.out.println("Closing the connection...");
                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 のターンバイターン合成など、合成タイミングを正確に制御する必要があるシナリオに適しています。

対話モードの切り替え

  • WebSocket: session.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 SDK: QwenTtsRealtimeConfig.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 リファレンス」をご参照ください。

高度な機能

命令ベースの制御

命令ベースの制御により、複雑な音声パラメーターを調整することなく、自然言語の記述を通じてトーン、速度、感情、音色を形成できます。

モデルごとの命令の仕様

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 音声リスト」をご参照ください。

使用方法instructions パラメーターを通じて命令の内容を指定します。

命令テキストでサポートされている言語

  • 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 シリーズのモデルのみがサポートされています。

使用方法instruction パラメーターを通じて命令の内容を指定します。

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

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

利用シーン

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

  • 広告・宣伝ナレーション

  • ゲームキャラクター・アニメーションの吹き替え

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

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

高品質な音声記述を作成するためのヒント

  • 基本原則

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

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

    3. 客観的であり、主観的でないこと:声の物理的および知覚的な性質に焦点を当てます。例えば、「私のお気に入りの声」ではなく、「少し高めのピッチでエネルギッシュ」のように記述します。

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

    5. 簡潔であり、冗長でないこと:すべての言葉に意味を持たせます。同義語を繰り返したり、意味のない修飾子を重ねたりすることは避けてください。

  • 記述の次元

    以下の次元を組み合わせることで、より正確な結果が得られます。記述する次元が多いほど、出力はより正確になります。

    次元

    記述例

    性別

    男性、女性、中性

    年齢

    子供 (5-12)、ティーンエイジャー (13-18)、若者 (19-35)、中年 (36-55)、高齢者 (55+)

    ピッチ

    高い、中間、低い、やや高い、やや低い

    速度

    速い、普通、遅い、やや速い、やや遅い

    感情

    陽気、穏やか、優しい、真面目、活発、落ち着いた、癒し系

    音色

    磁気的、張りのある、ハスキー、まろやか、甘い、豊か、力強い

    利用シーン

    ニュース放送、広告、オーディオブック、アニメキャラクター、音声アシスタント、ドキュメンタリーナレーション

    • 標準的な放送スタイル:標準的な発音で、明瞭かつ正確なアーティキュレーション

    • 若々しく活発な女性の声、やや速いペースで、顕著な上昇イントネーションがあり、ファッション製品の紹介に適している

    • 落ち着いた中年の男性の声、遅いペースで、深く磁気的な音色、ニュースの読み上げやドキュメンタリーのナレーションに適している

    • 優しく知的な女性の声、30歳前後、落ち着いたトーンで、オーディオブックの読み聞かせに適している

    • かわいい子供の声、8歳くらいの女の子、少し子供っぽい話し方で、アニメキャラクターの吹き替えに適している

方言

モデルを使用して、河南、四川、広東など中国語の方言で音声を出力します。設定はモデルと音声タイプによって異なります。

モデルごとの方言設定

CosyVoice

  • システム音声:「CosyVoice 音声リスト」から以下のいずれかの音声を選択します:

    • 方言サポートが組み込まれている音声 (例:longshange_v3) は、追加設定なしでその方言を出力します。

    • 命令ベースの制御をサポートし、方言選択が可能な音声 (例:longanhuan_v3):命令テキストでターゲットの方言を指定します。

  • 音声クローン音色命令ベースの制御を使用して方言を設定します — 例えば、命令テキストを 请用河南话表达 に設定します。

  • 音声デザイン音色:方言はまだサポートされていません。

モデルごとのサポートされている方言:「CosyVoice」の各モデルの「サポートされている言語」エントリをご参照ください。

:河南方言の音声を生成するには、cosyvoice-v3-flash モデルと longanhuan_v3 音声を使用し、命令テキストを "请用河南话表达。" に設定します。

# coding=utf-8

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

# API キーはシンガポールリージョンと北京リージョンで異なります。API キーの取得:https://www.alibabacloud.com/help/model-studio/get-api-key
# 環境変数が設定されていない場合は、次の行を Model Studio API キーに置き換えてください:dashscope.api_key = "sk-xxx"
dashscope.api_key = os.environ.get('DASHSCOPE_API_KEY')

# シンガポールリージョンの URL。WorkspaceId を実際のワークスペース ID に置き換えてください。URL はリージョンによって異なります。
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 package delay: {} 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」の各モデルの「サポートされている言語」エントリをご参照ください。

Raw WebSocket プロトコル

以下の例は、DashScope SDK を使用しないシナリオで、Raw WebSocket プロトコルを介してサーバーに直接接続する方法を示しています。これらは最小限の動作実装です。各モデルの WebSocket プロトコル仕様については、対応する API リファレンスをご参照ください。

Raw WebSocket プロトコルの例を表示

CosyVoice

Go

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/google/uuid"
	"github.com/gorilla/websocket"
)

const (
	// シンガポールリージョンの URL。WorkspaceId を実際のワークスペース ID に置き換えてください。URL はリージョンによって異なります。
	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":      "cosyvoice-v3-flash",
			"parameters": map[string]interface{}{
				"text_type":   "PlainText",
				"voice":       "longanyang",
				"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{"Before my bed, moonlight shines bright, I suspect it's frost upon the ground.", "I raise my eyes to gaze at the bright moon, then bow my head, thinking of home."}

							for _, text := range texts {
								continueTaskCmd := map[string]interface{}{
									"header": map[string]interface{}{
										"action":    "continue-task",
										"task_id":   taskID,
										"streaming": "duplex",
									},
									"payload": map[string]interface{}{
										"input": map[string]interface{}{
											"text": text,
										},
									},
								}

								continueTaskJSON, _ := json.Marshal(continueTaskCmd)
								fmt.Printf("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 environment variable is not set.");

    // シンガポールリージョンの URL。WorkspaceId を実際のワークスペース ID に置き換えてください。URL はリージョンによって異なります。
    private const string WebSocketUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference/";
    // 出力ファイルパス
    private const string OutputFilePath = "output.mp3";

    // WebSocket クライアント
    private static ClientWebSocket _webSocket = new ClientWebSocket();
    // キャンセルトークンソース
    private static CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
    // タスク ID
    private static string? _taskId;
    // タスクが開始されたかどうか
    private static TaskCompletionSource<bool> _taskStartedTcs = new TaskCompletionSource<bool>();

    static async Task Main(string[] args) {
        try {
            // 出力ファイルをクリア
            ClearOutputFile(OutputFilePath);

            // WebSocket サービスに接続
            await ConnectToWebSocketAsync(WebSocketUrl);

            // メッセージ受信タスクを開始
            Task receiveTask = ReceiveMessagesAsync();

            // run-task イベントを送信
            _taskId = GenerateTaskId();
            await SendRunTaskCommandAsync(_taskId);

            // task-started イベントを待機
            await _taskStartedTcs.Task;

            // continue-task イベントを継続的に送信
            string[] texts = {
                "Before my bed, moonlight shines bright",
                "I suspect it's frost upon the ground",
                "I raise my eyes to gaze at the bright moon",
                "then bow my head, thinking of home"
            };
            foreach (string text in texts) {
                await SendContinueTaskCommandAsync(text);
            }

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

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

            Console.WriteLine("タスクが完了し、接続が閉じられました。");
        } 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 = "cosyvoice-v3-flash",
            parameters = new
            {
                text_type = "PlainText",
                voice = "longanyang",
                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");
// シンガポールリージョンの URL。WorkspaceId を実際のワークスペース ID に置き換えてください。URL はリージョンによって異なります。
$websocket_url = 'wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference/'; // WebSocket サーバーアドレス
$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 = ["Before my bed, moonlight shines bright", "I suspect it's frost upon the ground", "I raise my eyes to gaze at the bright moon", "then bow my head, thinking of home"];
        $continueTaskCount = 0;
        foreach ($texts as $text) {
            $continueTaskMessage = json_encode([
                "header" => [
                    "action" => "continue-task",
                    "task_id" => $taskId,
                    "streaming" => "duplex"
                ],
                "payload" => [
                    "input" => [
                        "text" => $text
                    ]
                ]
            ]);
            echo "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" => "cosyvoice-v3-flash",
            "parameters" => [
                "text_type" => "PlainText",
                "voice" => "longanyang",
                "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;
// シンガポールリージョンの URL。WorkspaceId を実際のワークスペース ID に置き換えてください。URL はリージョンによって異なります。
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: 'cosyvoice-v3-flash',
      parameters: {
        text_type: 'PlainText',
        voice: 'longanyang', // 音声
        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 = [
    'Before my bed, moonlight shines bright,',
    'I suspect it\'s frost upon the ground.',
    'I raise my eyes to gaze at the bright moon,',
    'then bow my head, thinking of home.'
  ];

  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 の場合、Java DashScope SDK の使用を推奨します。詳細については、「Java SDK」をご参照ください。

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

  • Java-WebSocket

  • jackson-databind

Maven または Gradle を使用して依存関係を管理します。設定は以下の通りです:

pom.xml

<dependencies>
    <!-- WebSocket Client -->
    <dependency>
        <groupId>org.java-websocket</groupId>
        <artifactId>Java-WebSocket</artifactId>
        <version>1.5.3</version>
    </dependency>

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

build.gradle

// 他のコードは省略
dependencies {
  // WebSocket Client
  implementation 'org.java-websocket:Java-WebSocket:1.5.3'
  // JSON Processing
  implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.0'
}
// 他のコードは省略

Java コード:

import com.fasterxml.jackson.databind.ObjectMapper;

import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;

import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.*;

public class TTSWebSocketClient extends WebSocketClient {
    private final String taskId = UUID.randomUUID().toString();
    private final String outputFile = "output_" + System.currentTimeMillis() + ".mp3";
    private boolean taskFinished = false;

    public TTSWebSocketClient(URI serverUri, Map<String, String> headers) {
        super(serverUri, headers);
    }

    @Override
    public void onOpen(ServerHandshake serverHandshake) {
        System.out.println("接続が確立されました");

        // 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\": \"cosyvoice-v3-flash\", \"parameters\": { \"text_type\": \"PlainText\", \"voice\": \"longanyang\", \"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(
                                "Before my bed, moonlight shines bright, I suspect it's frost upon the ground",
                                "I raise my eyes to gaze at the bright moon, then bow my head, thinking of home"
                        );

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

                        // finish-task イベントを送信
                        sendFinishTask();
                    } else if ("task-finished".equals(event)) {
                        System.out.println("サーバーから 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);
            // シンガポールリージョンの URL。WorkspaceId を実際のワークスペース ID に置き換えてください。URL はリージョンによって異なります。
            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 の場合、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 インスタンスを初期化します。

    Parameters:
        api_key (str): 認証用の API キー
        uri (str): WebSocket サービスアドレス
    """
        self.api_key = api_key  # API キーに置き換えてください
        self.uri = uri  # WebSocket アドレスに置き換えてください
        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": "cosyvoice-v3-flash",
                "parameters": {
                    "text_type": "PlainText",
                    "voice": "longanyang",
                    "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": {}
            }
        }

        # 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 = [
                                "Before my bed, moonlight shines bright, I suspect it's frost upon the ground",
                                "I raise my eyes to gaze at the bright moon, then bow my head, thinking of home"
                            ]

                            for text in texts:
                                self.send_continue_task(text)

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

                        elif event == "task-finished":
                            print("タスクが完了しました")
                            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")
    # シンガポールリージョンの URL。WorkspaceId を実際のワークスペース ID に置き換えてください。URL はリージョンによって異なります。
    SERVER_URI = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference/"  # WebSocket アドレスに置き換えてください

    client = TTSClient(API_KEY, SERVER_URI)
    client.run()

Qwen-TTS

  1. クライアントの作成

    Python

    tts_realtime_client.py という名前の Python ファイルを作成し、次のコードをコピーします:

    # -- coding: utf-8 --
    
    import asyncio
    import websockets
    import json
    import base64
    import time
    from typing import Optional, Callable, Dict, Any
    from enum import Enum
    
    class SessionMode(Enum):
        SERVER_COMMIT = "server_commit"
        COMMIT = "commit"
    
    class TTSRealtimeClient:
        """
        TTS Realtime API と対話するためのクライアント。
    
        このクラスは、TTS Realtime API への接続、テキストデータの送信、
        音声出力の受信、および WebSocket 接続の管理のためのメソッドを提供します。
    
        Attributes:
            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": "Speak quickly with a noticeable rising intonation, suitable for introducing fashion products.",
                # "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"Sending event: type={event['type']}, event_id={event['event_id']}")
            await self.ws.send(json.dumps(event))
    
        async def update_session(self, config: Dict[str, Any]) -> None:
            """セッション設定を更新します。"""
            event = {
                "type": "session.update",
                "session": config
            }
            print("Updating session configuration: ", event)
            await self.send_event(event)
    
        async def append_text(self, text: str) -> None:
            """API にテキストデータを送信します。"""
            event = {
                "type": "input_text_buffer.append",
                "text": text
            }
            await self.send_event(event)
    
        async def commit_text_buffer(self) -> None:
            """テキストバッファーをコミットして処理をトリガーします。"""
            event = {
                "type": "input_text_buffer.commit"
            }
            await self.send_event(event)
    
        async def clear_text_buffer(self) -> None:
            """テキストバッファーをクリアします。"""
            event = {
                "type": "input_text_buffer.clear"
            }
            await self.send_event(event)
    
        async def finish_session(self) -> None:
            """セッションを終了します。"""
            event = {
                "type": "session.finish"
            }
            await self.send_event(event)
    
        async def wait_for_response_done(self):
            """response.done イベントを待ちます"""
            if self._response_done_future:
                await self._response_done_future
    
        async def handle_messages(self) -> None:
            """サーバーからのメッセージを処理します。"""
            try:
                async for message in self.ws:
                    event = json.loads(message)
                    event_type = event.get("type")
    
                    if event_type != "response.audio.delta":
                        print(f"Received event: {event_type}")
    
                    if event_type == "error":
                        print("Error: ", event.get('error', {}))
                        continue
                    elif event_type == "session.created":
                        print("Session created, ID: ", event.get('session', {}).get('id'))
                    elif event_type == "session.updated":
                        print("Session updated, ID: ", event.get('session', {}).get('id'))
                    elif event_type == "input_text_buffer.committed":
                        print("Text buffer committed, item ID: ", event.get('item_id'))
                    elif event_type == "input_text_buffer.cleared":
                        print("Text buffer cleared")
                    elif event_type == "response.created":
                        self._current_response_id = event.get("response", {}).get("id")
                        self._is_responding = True
                        # response.done を待つための新しい future を作成
                        self._response_done_future = asyncio.Future()
                        print("Response created, ID: ", self._current_response_id)
                    elif event_type == "response.output_item.added":
                        self._current_item_id = event.get("item", {}).get("id")
                        print("Output item added, ID: ", self._current_item_id)
                    # 音声デルタを処理
                    elif event_type == "response.audio.delta" and self.audio_callback:
                        audio_bytes = base64.b64decode(event.get("delta", ""))
                        self.audio_callback(audio_bytes)
                    elif event_type == "response.audio.done":
                        print("Audio generation completed")
                    elif event_type == "response.done":
                        self._is_responding = False
                        self._current_response_id = None
                        self._current_item_id = None
                        # future を完了としてマーク
                        if self._response_done_future and not self._response_done_future.done():
                            self._response_done_future.set_result(True)
                        print("Response completed")
                    elif event_type == "session.finished":
                        print("Session ended")
    
            except websockets.exceptions.ConnectionClosed:
                print("Connection closed")
            except Exception as e:
                print("Error processing messages: ", str(e))
    
        async def close(self) -> None:
            """WebSocket 接続を閉じます。"""
            if self.ws:
                await self.ws.close()

    Java

    TTSRealtimeClient.java という名前の Java ファイルを作成し、次のコードをコピーします:

    import com.google.gson.Gson;
    import com.google.gson.JsonObject;
    import org.java_websocket.client.WebSocketClient;
    import org.java_websocket.handshake.ServerHandshake;
    
    import java.net.URI;
    import java.util.Base64;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.concurrent.CountDownLatch;
    import java.util.function.Consumer;
    
    /**
     * TTS 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 connection established");
                    // デフォルトのセッション設定を送信
                    JsonObject session = new JsonObject();
                    session.addProperty("mode", mode.getValue());
                    session.addProperty("voice", TTSRealtimeClient.this.voice);
                    // 命令制御機能を使用するには、以下の行のコメントを解除し、モデルを qwen3-tts-instruct-flash-realtime に置き換えます
                    // session.addProperty("instructions", "Speak quickly with a noticeable rising intonation, suitable for introducing fashion products.");
                    // 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("Received event: " + eventType);
                    }
    
                    switch (eventType) {
                        case "error":
                            System.err.println("Error: " + event.get("error"));
                            break;
                        case "session.created":
                            System.out.println("Session created, ID: " +
                                event.getAsJsonObject("session").get("id").getAsString());
                            break;
                        case "session.updated":
                            System.out.println("Session updated, ID: " +
                                event.getAsJsonObject("session").get("id").getAsString());
                            break;
                        case "input_text_buffer.committed":
                            System.out.println("Text buffer committed, item ID: " + event.get("item_id"));
                            break;
                        case "input_text_buffer.cleared":
                            System.out.println("Text buffer cleared");
                            break;
                        case "response.created":
                            System.out.println("Response created, ID: " +
                                event.getAsJsonObject("response").get("id").getAsString());
                            responseDoneLatch = new CountDownLatch(1);
                            break;
                        case "response.output_item.added":
                            System.out.println("Output item added, ID: " +
                                event.getAsJsonObject("item").get("id").getAsString());
                            break;
                        case "response.audio.delta":
                            if (audioCallback != null) {
                                byte[] audioBytes = Base64.getDecoder().decode(
                                    event.get("delta").getAsString());
                                audioCallback.onAudio(audioBytes);
                            }
                            break;
                        case "response.audio.done":
                            System.out.println("Audio generation completed");
                            break;
                        case "response.done":
                            System.out.println("Response completed");
                            responseDoneLatch.countDown();
                            break;
                        case "session.finished":
                            System.out.println("Session ended");
                            sessionFinishedLatch.countDown();
                            break;
                    }
                }
    
                @Override
                public void onClose(int code, String reason, boolean remote) {
                    System.out.println("Connection closed: " + reason);
                }
    
                @Override
                public void onError(Exception ex) {
                    System.err.println("WebSocket error: " + ex.getMessage());
                }
            };
            ws.connectBlocking();
        }
    
        /**
         * サーバーにイベントを送信します。
         */
        public void sendEvent(JsonObject event) {
            String eventId = "event_" + System.currentTimeMillis();
            event.addProperty("event_id", eventId);
            System.out.println("Sending event: type=" + event.get("type").getAsString()
                + ", event_id=" + eventId);
            ws.send(gson.toJson(event));
        }
    
        /**
         * セッション設定を更新します。
         */
        public void updateSession(JsonObject config) {
            JsonObject event = new JsonObject();
            event.addProperty("type", "session.update");
            event.add("session", config);
            System.out.println("Updating session configuration: " + event);
            sendEvent(event);
        }
    
        /**
         * API にテキストデータを送信します。
         */
        public void appendText(String text) {
            JsonObject event = new JsonObject();
            event.addProperty("type", "input_text_buffer.append");
            event.addProperty("text", text);
            sendEvent(event);
        }
    
        /**
         * テキストバッファーをコミットして処理をトリガーします。
         */
        public void commitTextBuffer() {
            JsonObject event = new JsonObject();
            event.addProperty("type", "input_text_buffer.commit");
            sendEvent(event);
        }
    
        /**
         * テキストバッファーをクリアします。
         */
        public void clearTextBuffer() {
            JsonObject event = new JsonObject();
            event.addProperty("type", "input_text_buffer.clear");
            sendEvent(event);
        }
    
        /**
         * セッションを終了します。
         */
        public void finishSession() {
            JsonObject event = new JsonObject();
            event.addProperty("type", "session.finish");
            sendEvent(event);
        }
    
        /**
         * response.done イベントを待ちます。
         */
        public void waitForResponseDone() throws InterruptedException {
            responseDoneLatch.await();
        }
    
        /**
         * session.finished イベントを待ちます。
         */
        public void waitForSessionFinished() throws InterruptedException {
            sessionFinishedLatch.await();
        }
    
        /**
         * WebSocket 接続を閉じます。
         */
        public void close() {
            if (ws != null) {
                ws.close();
            }
        }
    }
  2. 合成モードの選択

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

    • `server_commit` モード

      クライアントはテキストのみを送信します。サーバーは分割と合成のタイミングをインテリジェントに決定します。このモードは、GPS ナビゲーションなど、合成リズムの手動制御を必要としない低レイテンシーのシナリオに適しています。

    • `commit` モード

      クライアントはテキストをバッファーに追加し、その後、指定されたテキストを合成するようにサーバーをトリガーします。このモードは、ニュース放送など、文の区切りや間を細かく制御する必要があるシナリオに適しています。

    server_commit モード

    Python

    tts_realtime_client.py と同じディレクトリに、server_commit.py という名前の別の Python ファイルを作成し、次のコードをコピーします:

    import os
    import asyncio
    import logging
    import wave
    from tts_realtime_client import TTSRealtimeClient, SessionMode
    import pyaudio
    
    # QwenTTS サービス設定
    # 命令制御機能を使用するには、モデルを qwen3-tts-instruct-flash-realtime に置き換え、tts_realtime_client.py の instructions のコメントを解除します
    # シンガポールリージョンの URL。WorkspaceId を実際のワークスペース ID に置き換えてください。URL はリージョンによって異なります。
    URL = "wss://{WorkspaceId}.ap-southeast-1.maas.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("Please set DASHSCOPE_API_KEY environment variable")
    
    # 音声データを収集
    _audio_chunks = []
    # リアルタイム再生関連
    _AUDIO_SAMPLE_RATE = 24000
    _audio_pyaudio = pyaudio.PyAudio()
    _audio_stream = None  # 実行時に開かれます
    
    def _audio_callback(audio_bytes: bytes):
        """TTSRealtimeClient 音声コールバック:リアルタイム再生とキャッシュ"""
        global _audio_stream
        if _audio_stream is not None:
            try:
                _audio_stream.write(audio_bytes)
            except Exception as exc:
                logging.error(f"PyAudio playback error: {exc}")
        _audio_chunks.append(audio_bytes)
        logging.info(f"Received audio chunk: {len(audio_bytes)} bytes")
    
    def _save_audio_to_file(filename: str = "output.wav", sample_rate: int = 24000) -> bool:
        """収集した音声データを WAV ファイルとして保存"""
        if not _audio_chunks:
            logging.warning("No audio data to save")
            return False
    
        try:
            audio_data = b"".join(_audio_chunks)
            with wave.open(filename, 'wb') as wav_file:
                wav_file.setnchannels(1)  # モノラル
                wav_file.setsampwidth(2)  # 16 ビット
                wav_file.setframerate(sample_rate)
                wav_file.writeframes(audio_data)
            logging.info(f"Audio saved to: {filename}")
            return True
        except Exception as exc:
            logging.error(f"Failed to save audio: {exc}")
            return False
    
    async def _produce_text(client: TTSRealtimeClient):
        """サーバーにテキストフラグメントを送信"""
        text_fragments = [
            "Alibaba Cloud's large language model platform, Model Studio, is an all-in-one platform for developing and building large language model applications.",
            "Both developers and business users can deeply participate in the design and development of large language model applications.",
            "You can develop a large language model application in five minutes using a simple interface,",
            "or train a dedicated model in a few hours, allowing you to focus more energy on application innovation."
        ]
    
        logging.info("Sending text fragments…")
        for text in text_fragments:
            logging.info(f"Sending fragment: {text}")
            await client.append_text(text)
            await asyncio.sleep(0.1)  # フラグメント間の短い遅延
    
        # セッションを終了する前にサーバーが内部処理を完了するのを待つ
        await asyncio.sleep(1.0)
        await client.finish_session()
    
    async def _run_demo():
        """完全なデモを実行"""
        global _audio_stream
        # PyAudio 出力ストリームを開く
        _audio_stream = _audio_pyaudio.open(
            format=pyaudio.paInt16,
            channels=1,
            rate=_AUDIO_SAMPLE_RATE,
            output=True,
            frames_per_buffer=1024
        )
    
        client = TTSRealtimeClient(
            base_url=URL,
            api_key=API_KEY,
            voice="Cherry",
            mode=SessionMode.SERVER_COMMIT,
            audio_callback=_audio_callback
        )
    
        # 接続を確立
        await client.connect()
    
        # メッセージ処理とテキスト送信を並行して実行
        consumer_task = asyncio.create_task(client.handle_messages())
        producer_task = asyncio.create_task(_produce_text(client))
    
        await producer_task  # テキスト送信が完了するのを待つ
    
        # response.done を待つ
        await client.wait_for_response_done()
    
        # 接続を閉じ、コンシューマータスクをキャンセル
        await client.close()
        consumer_task.cancel()
    
        # オーディオストリームを閉じる
        if _audio_stream is not None:
            _audio_stream.stop_stream()
            _audio_stream.close()
        _audio_pyaudio.terminate()
    
        # 音声データを保存
        os.makedirs("outputs", exist_ok=True)
        _save_audio_to_file(os.path.join("outputs", "qwen_tts_output.wav"))
    
    def main():
        """同期エントリポイント"""
        logging.basicConfig(
            level=logging.INFO,
            format='%(asctime)s [%(levelname)s] %(message)s',
            datefmt='%Y-%m-%d %H:%M:%S'
        )
        logging.info("Starting QwenTTS Realtime Client demo…")
        asyncio.run(_run_demo())
    
    if __name__ == "__main__":
        main()

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

    Java

    TTSRealtimeClient.java と同じディレクトリに、ServerCommit.java という名前の別の Java ファイルを作成し、次のコードをコピーします:

    import javax.sound.sampled.*;
    import java.io.*;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.ConcurrentLinkedQueue;
    import java.util.concurrent.atomic.AtomicBoolean;
    
    public class ServerCommit {
        // シンガポールリージョンの URL。WorkspaceId を実際のワークスペース ID に置き換えてください。URL はリージョンによって異なります。
        private static final String URL = "wss://{WorkspaceId}.ap-southeast-1.maas.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("Please set the DASHSCOPE_API_KEY environment variable");
            }
    
            // 音声再生を初期化
            AudioFormat format = new AudioFormat(SAMPLE_RATE, 16, 1, true, false);
            DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);
            SourceDataLine audioLine = (SourceDataLine) AudioSystem.getLine(info);
            audioLine.open(format);
            audioLine.start();
    
            // 再生スレッドを開始
            Thread playerThread = new Thread(() -> {
                while (playing.get() || !playbackQueue.isEmpty()) {
                    byte[] chunk = playbackQueue.poll();
                    if (chunk != null) {
                        audioLine.write(chunk, 0, chunk.length);
                    } else {
                        try { Thread.sleep(10); } catch (InterruptedException ignored) {}
                    }
                }
            });
            playerThread.start();
    
            // TTS クライアントを作成
            // 命令制御機能を使用するには、モデルを 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("Received audio data: " + audioData.length + " bytes");
                }
            );
    
            client.connect();
    
            // テキストフラグメントを送信
            String[] textFragments = {
                "Alibaba Cloud's large language model platform, Model Studio, is an all-in-one platform for developing and building large language model applications.",
                "Both developers and business users can deeply participate in the design and development of large language model applications.",
                "You can develop a large language model application in five minutes using a simple interface,",
                "or train a dedicated model in a few hours, allowing you to focus more energy on application innovation."
            };
    
            System.out.println("Sending text...");
            for (String text : textFragments) {
                System.out.println("Sending fragment: " + text);
                client.appendText(text);
                Thread.sleep(100);
            }
    
            Thread.sleep(1000);
            client.finishSession();
    
            // 応答が完了するのを待つ
            client.waitForResponseDone();
            client.waitForSessionFinished();
            client.close();
    
            // 再生が完了するのを待つ
            playing.set(false);
            playerThread.join();
            audioLine.drain();
            audioLine.close();
    
            // 音声ファイルを保存
            saveWav("output.wav");
            System.out.println("Done");
        }
    
        private static void saveWav(String filename) throws IOException {
            if (audioChunks.isEmpty()) {
                System.out.println("No audio data to save");
                return;
            }
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            for (byte[] chunk : audioChunks) {
                bos.write(chunk);
            }
            byte[] allAudio = bos.toByteArray();
            AudioFormat format = new AudioFormat(SAMPLE_RATE, 16, 1, true, false);
            AudioInputStream ais = new AudioInputStream(
                new ByteArrayInputStream(allAudio), format, allAudio.length / 2);
            new File("outputs").mkdirs();
            AudioSystem.write(ais, AudioFileFormat.Type.WAVE,
                new File("outputs/" + filename));
            System.out.println("Audio saved to: outputs/" + filename);
        }
    }

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

    commit モード

    Python

    tts_realtime_client.py と同じディレクトリに、commit.py という名前の別の Python ファイルを作成し、次のコードをコピーします:

    import os
    import asyncio
    import logging
    import wave
    from tts_realtime_client import TTSRealtimeClient, SessionMode
    import pyaudio
    
    # QwenTTS サービス設定
    # 命令制御機能を使用するには、モデルを qwen3-tts-instruct-flash-realtime に置き換え、tts_realtime_client.py の instructions のコメントを解除します
    # シンガポールリージョンの URL。WorkspaceId を実際のワークスペース ID に置き換えてください。URL はリージョンによって異なります。
    URL = "wss://{WorkspaceId}.ap-southeast-1.maas.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("Please set DASHSCOPE_API_KEY environment variable")
    
    # 音声データを収集
    _audio_chunks = []
    _AUDIO_SAMPLE_RATE = 24000
    _audio_pyaudio = pyaudio.PyAudio()
    _audio_stream = None
    
    def _audio_callback(audio_bytes: bytes):
        """TTSRealtimeClient 音声コールバック:リアルタイム再生とキャッシュ"""
        global _audio_stream
        if _audio_stream is not None:
            try:
                _audio_stream.write(audio_bytes)
            except Exception as exc:
                logging.error(f"PyAudio playback error: {exc}")
        _audio_chunks.append(audio_bytes)
        logging.info(f"Received audio chunk: {len(audio_bytes)} bytes")
    
    def _save_audio_to_file(filename: str = "output.wav", sample_rate: int = 24000) -> bool:
        """収集した音声データを WAV ファイルとして保存"""
        if not _audio_chunks:
            logging.warning("No audio data to save")
            return False
    
        try:
            audio_data = b"".join(_audio_chunks)
            with wave.open(filename, 'wb') as wav_file:
                wav_file.setnchannels(1)  # モノラル
                wav_file.setsampwidth(2)  # 16 ビット
                wav_file.setframerate(sample_rate)
                wav_file.writeframes(audio_data)
            logging.info(f"Audio saved to: {filename}")
            return True
        except Exception as exc:
            logging.error(f"Failed to save audio: {exc}")
            return False
    
    async def _user_input_loop(client: TTSRealtimeClient):
        """ユーザー入力を継続的に取得し、テキストを送信します。ユーザーが空のテキストを入力すると、コミットイベントを送信してセッションを終了します。"""
        print("Enter text (press Enter directly to send a commit event and end the session, press Ctrl+C or Ctrl+D to exit the program):")
    
        while True:
            try:
                user_text = input("> ")
                if not user_text:  # ユーザー入力が空
                    # 空の入力は会話の終了を示します:バッファーをコミット -> セッションを終了 -> ループを終了
                    logging.info("Empty input, sending commit event and ending session")
                    await client.commit_text_buffer()
                    # サーバーがコミットを処理するのを少し待ってから、音声が失われる可能性のある早すぎるセッション終了を防ぎます
                    await asyncio.sleep(0.3)
                    await client.finish_session()
                    break  # ユーザー入力ループを直接終了し、再度 Enter を押す必要はありません
                else:
                    logging.info(f"Sending text: {user_text}")
                    await client.append_text(user_text)
    
            except EOFError:  # ユーザーが Ctrl+D を押した
                break
            except KeyboardInterrupt:  # ユーザーが Ctrl+C を押した
                break
    
        # セッションを終了
        logging.info("Ending 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.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("Starting QwenTTS Realtime Client demo…")
        asyncio.run(_run_demo())
    
    if __name__ == "__main__":
        main()

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

    Java

    TTSRealtimeClient.java と同じディレクトリに、Commit.java という名前の別の Java ファイルを作成し、次のコードをコピーします:

    import javax.sound.sampled.*;
    import java.io.*;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Scanner;
    import java.util.concurrent.ConcurrentLinkedQueue;
    import java.util.concurrent.atomic.AtomicBoolean;
    
    public class Commit {
        // シンガポールリージョンの URL。WorkspaceId を実際のワークスペース ID に置き換えてください。URL はリージョンによって異なります。
        private static final String URL = "wss://{WorkspaceId}.ap-southeast-1.maas.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("Please set the DASHSCOPE_API_KEY environment variable");
            }
    
            // 音声再生を初期化
            AudioFormat format = new AudioFormat(SAMPLE_RATE, 16, 1, true, false);
            DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);
            SourceDataLine audioLine = (SourceDataLine) AudioSystem.getLine(info);
            audioLine.open(format);
            audioLine.start();
    
            // 再生スレッドを開始
            Thread playerThread = new Thread(() -> {
                while (playing.get() || !playbackQueue.isEmpty()) {
                    byte[] chunk = playbackQueue.poll();
                    if (chunk != null) {
                        audioLine.write(chunk, 0, chunk.length);
                    } else {
                        try { Thread.sleep(10); } catch (InterruptedException ignored) {}
                    }
                }
            });
            playerThread.start();
    
            // TTS クライアントを作成 (コミットモード)
            // 命令制御機能を使用するには、モデルを 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("Received audio data: " + audioData.length + " bytes");
                }
            );
    
            client.connect();
    
            // 対話型入力
            System.out.println("Enter text (press Enter directly to send a commit event and end the session, press Ctrl+D to exit the program):");
            Scanner scanner = new Scanner(System.in);
            while (true) {
                System.out.print("> ");
                if (!scanner.hasNextLine()) {
                    client.finishSession();
                    break;
                }
                String userText = scanner.nextLine();
                if (userText.isEmpty()) {
                    // 空の入力:バッファーをコミットしてセッションを終了
                    System.out.println("Empty input, sending commit event and ending session");
                    client.commitTextBuffer();
                    Thread.sleep(300);
                    client.finishSession();
                    break;
                } else {
                    System.out.println("Sending text: " + userText);
                    client.appendText(userText);
                }
            }
            scanner.close();
    
            // 応答が完了するのを待つ
            client.waitForResponseDone();
            client.waitForSessionFinished();
            client.close();
    
            // 再生が完了するのを待つ
            playing.set(false);
            playerThread.join();
            audioLine.drain();
            audioLine.close();
    
            // 音声ファイルを保存
            saveWav("output.wav");
            System.out.println("Done");
        }
    
        private static void saveWav(String filename) throws IOException {
            if (audioChunks.isEmpty()) {
                System.out.println("No audio data to save");
                return;
            }
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            for (byte[] chunk : audioChunks) {
                bos.write(chunk);
            }
            byte[] allAudio = bos.toByteArray();
            AudioFormat format = new AudioFormat(SAMPLE_RATE, 16, 1, true, false);
            AudioInputStream ais = new AudioInputStream(
                new ByteArrayInputStream(allAudio), format, allAudio.length / 2);
            new File("outputs").mkdirs();
            AudioSystem.write(ais, AudioFileFormat.Type.WAVE,
                new File("outputs/" + filename));
            System.out.println("Audio saved to: outputs/" + filename);
        }
    }

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

Sambert

Go

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"time"

	"github.com/google/uuid"
	"github.com/gorilla/websocket"
)

const (
	wsURL      = "wss://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference/" // WebSocket サーバーアドレス
	outputFile = "output.mp3"                                        // 出力ファイルパス
)

func main() {
	// API キーが環境変数に設定されていない場合は、次の行を apiKey := "your_api_key" に置き換えてください。本番コードでは API キーを直接ハードコーディングすることは、API キー漏洩のリスクを減らすため推奨されません。
	apiKey := os.Getenv("DASHSCOPE_API_KEY")

	// 出力ファイルの確認とクリア
	if err := clearOutputFile(outputFile); err != nil {
		fmt.Println("出力ファイルのクリアに失敗しました:", err)
		return
	}

	// WebSocket サービスに接続
	conn, err := connectWebSocket(apiKey)
	if err != nil {
		fmt.Println("WebSocket への接続に失敗しました:", err)
		return
	}
	defer closeConnection(conn)

	// タスク完了通知を受信するためのチャネルを作成
	done := make(chan struct{})

	// メッセージを受信するための非同期ゴルーチンを開始
	go receiveMessage(conn, done)

	// run-task コマンドを送信
	if err := sendRunTaskMsg(conn); err != nil {
		fmt.Println("run-task コマンドの送信に失敗しました:", err)
		return
	}

	// タスク完了またはタイムアウトを待機
	select {
	case <-done:
		fmt.Println("タスクが終了しました")
	case <-time.After(5 * time.Minute):
		fmt.Println("タスクがタイムアウトしました")
	}
}

// メッセージ構造体の定義
type Message struct {
	Header  Header  `json:"header"`
	Payload Payload `json:"payload"`
}

// ヘッダーの定義
type Header struct {
	Action       string                 `json:"action,omitempty"`
	TaskID       string                 `json:"task_id"`
	Streaming    string                 `json:"streaming,omitempty"`
	Event        string                 `json:"event,omitempty"`
	ErrorCode    string                 `json:"error_code,omitempty"`
	ErrorMessage string                 `json:"error_message,omitempty"`
	Attributes   map[string]interface{} `json:"attributes"`
}

// ペイロードの定義
type Payload struct {
	Model      string     `json:"model,omitempty"`
	TaskGroup  string     `json:"task_group,omitempty"`
	Task       string     `json:"task,omitempty"`
	Function   string     `json:"function,omitempty"`
	Input      Input      `json:"input,omitempty"`
	Parameters Parameters `json:"parameters,omitempty"`
	Output     Output     `json:"output,omitempty"`
	Usage      Usage      `json:"usage,omitempty"`
}

// 入力の定義
type Input struct {
	Text string `json:"text"`
}

// パラメーターの定義
type Parameters struct {
	TextType                string  `json:"text_type"`
	Format                  string  `json:"format"`
	SampleRate              int     `json:"sample_rate"`
	Volume                  int     `json:"volume"`
	Rate                    float64 `json:"rate"`
	Pitch                   float64 `json:"pitch"`
	WordTimestampEnabled    bool    `json:"word_timestamp_enabled"`
	PhonemeTimestampEnabled bool    `json:"phoneme_timestamp_enabled"`
}

// 出力の定義
type Output struct {
	Sentence Sentence `json:"sentence"`
}

// 文の定義
type Sentence struct {
	BeginTime int    `json:"begin_time"`
	EndTime   int    `json:"end_time"`
	Words     []Word `json:"words"`
}

// 単語の定義
type Word struct {
	Text      string    `json:"text"`
	BeginTime int       `json:"begin_time"`
	EndTime   int       `json:"end_time"`
	Phonemes  []Phoneme `json:"phonemes"`
}

// 音素の定義
type Phoneme struct {
	BeginTime int    `json:"begin_time"`
	EndTime   int    `json:"end_time"`
	Text      string `json:"text"`
	Tone      int    `json:"tone"`
}

// 使用量の定義
type Usage struct {
	Characters int `json:"characters"`
}

func receiveMessage(conn *websocket.Conn, done chan struct{}) {
	for {
		msgType, message, err := conn.ReadMessage()
		if err != nil {
			fmt.Println("サーバーメッセージの解析に失敗しました:", err)
			close(done)
			break
		}

		if msgType == websocket.BinaryMessage {
			// バイナリ音声ストリームを処理
			if err := writeBinaryDataToFile(message, outputFile); err != nil {
				fmt.Println("バイナリデータの書き込みに失敗しました:", err)
				close(done)
				break
			}
			fmt.Println("音声チャンクがローカルファイルに書き込まれました")
		} else {
			// テキストメッセージを処理
			var msg Message
			if err := json.Unmarshal(message, &msg); err != nil {
				fmt.Println("イベントの解析に失敗しました:", err)
				continue
			}
			if handleMessage(conn, msg, done) {
				break
			}
		}
	}
}

func handleMessage(conn *websocket.Conn, msg Message, done chan struct{}) bool {
	switch msg.Header.Event {
	case "task-started":
		fmt.Println("タスクが開始されました")

	case "result-generated":
	// 追加のメッセージを取得する必要がある場合は、ここにコードを追加します

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

	case "task-failed":
		if msg.Header.ErrorMessage != "" {
			fmt.Printf("タスクが失敗しました: %s\n", msg.Header.ErrorMessage)
		} else {
			fmt.Println("不明な理由でタスクが失敗しました")
		}
		close(done)
		return true

	default:
		fmt.Printf("予期しないイベント: %v\n", msg)
		close(done)
	}

	return false
}

func sendRunTaskMsg(conn *websocket.Conn) error {
	runTaskMsg, err := generateRunTaskMsg()
	if err != nil {
		return err
	}
	if err := conn.WriteMessage(websocket.TextMessage, []byte(runTaskMsg)); err != nil {
		return err
	}
	return nil
}

func generateRunTaskMsg() (string, error) {
	runTaskMessage := Message{
		Header: Header{
			Action:    "run-task",
			TaskID:    uuid.New().String(),
			Streaming: "out",
		},
		Payload: Payload{
			Model:     "sambert-zhichu-v1",
			TaskGroup: "audio",
			Task:      "tts",
			Function:  "SpeechSynthesizer",
			Input: Input{
				Text: "The white sun sets behind the mountains, and the Yellow River flows into the sea. To see a thousand miles further, you must climb one more story.",
			},
			Parameters: Parameters{
				TextType:                "PlainText",
				Format:                  "mp3",
				SampleRate:              16000,
				Volume:                  50,
				Rate:                    1.0,
				Pitch:                   1.0,
				WordTimestampEnabled:    true,
				PhonemeTimestampEnabled: true,
			},
		},
	}

	runTaskMsgJSON, err := json.Marshal(runTaskMessage)
	return string(runTaskMsgJSON), err
}

func connectWebSocket(apiKey string) (*websocket.Conn, error) {
	header := make(http.Header)
	header.Add("X-DashScope-DataInspection", "enable")
	header.Add("Authorization", fmt.Sprintf("bearer %s", apiKey))
	conn, _, err := websocket.DefaultDialer.Dial(wsURL, header)
	if err != nil {
		fmt.Println("WebSocket への接続に失敗しました:", err)
		return nil, err
	}
	return conn, nil
}

func writeBinaryDataToFile(data []byte, filePath string) error {
	file, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return err
	}
	defer file.Close()
	_, err = file.Write(data)
	return err
}

func closeConnection(conn *websocket.Conn) {
	if conn != nil {
		conn.Close()
	}
}

func clearOutputFile(filePath string) error {
	file, err := os.OpenFile(filePath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return err
	}
	file.Close()
	return nil
}

C#

サンプルコード:

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

class Program {
    // API キーが環境変数に設定されていない場合は、次の行を private const string ApiKey="your_api_key" に置き換えてください。本番コードでは API キーを直接ハードコーディングすることは、API キー漏洩のリスクを減らすため推奨されません。
    private static readonly string ApiKey = Environment.GetEnvironmentVariable("DASHSCOPE_API_KEY") ?? throw new InvalidOperationException("DASHSCOPE_API_KEY environment variable is not set.");

    private const string WebSocketUrl = "wss://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference/"; // WebSocket サーバーアドレス
    private const string OutputFilePath = "output.mp3"; // 出力ファイルパス

    static async Task Main(string[] args) {
        var ws = new ClientWebSocket();
        try {
            // 1. WebSocket サービスに接続し、認証します
            await ConnectWithAuth(ws, WebSocketUrl);

            // 2. メッセージ受信スレッドを開始します
            var receiveTask = ReceiveMessages(ws);

            // 3. run-task コマンドを送信します
            string textToSynthesize = "The white sun sets behind the mountains, and the Yellow River flows into the sea. To see a thousand miles further, you must climb one more story.";
            string taskId = GenerateTaskId();
            await SendRunTaskCommand(ws, textToSynthesize, taskId);

            // 4. 受信タスクが完了するのを待ちます
            await receiveTask;
        } catch (Exception ex) {
            Console.WriteLine($"Error: {ex.Message}");
        } finally {
            if (ws.State == WebSocketState.Open) {
                await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing connection", CancellationToken.None);
            }
        }
    }

    private static async Task ConnectWithAuth(ClientWebSocket ws, string url) {
        var uri = new Uri(url);
        ws.Options.SetRequestHeader("Authorization", $"bearer {ApiKey}");
        ws.Options.SetRequestHeader("X-DashScope-DataInspection", "enable");
        await ws.ConnectAsync(uri, CancellationToken.None);
        Console.WriteLine("Connected to WebSocket server.");
    }

    private static string GenerateTaskId() {
        return Guid.NewGuid().ToString("N");
    }

    private static async Task SendRunTaskCommand(ClientWebSocket ws, string text, string taskId) {
        var command = CreateRunTaskCommand(text, taskId);
        var buffer = Encoding.UTF8.GetBytes(command);
        await ws.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None);
        Console.WriteLine("run-task command sent.");
    }

    private static string CreateRunTaskCommand(string text, string taskId) {
        var command = new {
            header = new {
                action = "run-task",
                task_id = taskId,
                streaming = "out"
            },
            payload = new {
                model = "sambert-zhichu-v1",
                task_group = "audio",
                task = "tts",
                function = "SpeechSynthesizer",
                input = new {
                    text = text
                },
                parameters = new {
                    text_type = "PlainText",
                    format = "mp3",
                    sample_rate = 16000,
                    volume = 50,
                    rate = 1,
                    pitch = 1,
                    word_timestamp_enabled = true,
                    phoneme_timestamp_enabled = true
                }
            }
        };
        return JsonSerializer.Serialize(command);
    }

    private static async Task ReceiveMessages(ClientWebSocket ws) {
        var buffer = new byte[1024 * 4];
        var fs = new FileStream(OutputFilePath, FileMode.Create, FileAccess.Write);
        bool taskStarted = false;
        bool taskFinished = false;

        while (ws.State == WebSocketState.Open && !taskFinished) {
            var result = await ws.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);

            switch (result.MessageType) {
                case WebSocketMessageType.Text:
                    var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
                    var jsonMessage = JsonSerializer.Deserialize<JsonElement>(message);

                    ProcessTextMessage(jsonMessage, ref taskStarted, ref taskFinished);
                    break;
                case WebSocketMessageType.Binary:
                    if (taskStarted) {
                        await fs.WriteAsync(buffer, 0, result.Count);
                        Console.WriteLine("Received audio data.");
                    }
                    break;
                case WebSocketMessageType.Close:
                    Console.WriteLine("Server closed the connection.");
                    taskFinished = true;
                    break;
            }
        }
        fs.Close();
    }

    private static void ProcessTextMessage(JsonElement jsonMessage, ref bool taskStarted, ref bool taskFinished) {
        if (jsonMessage.TryGetProperty("header", out JsonElement header) && header.TryGetProperty("event", out JsonElement eventToken)) {
            var eventType = eventToken.GetString();
            switch (eventType) {
                case "task-started":
                    taskStarted = true;
                    Console.WriteLine("Task started.");
                    break;
                case "result-generated":
                    // 追加のメッセージを取得する必要がある場合は、ここにコードを追加します
                    break;
                case "task-finished":
                    taskFinished = true;
                    Console.WriteLine("Task completed.");
                    break;
                case "task-failed":
                    taskFinished = true;
                    Console.WriteLine("Task failed.");
                    break;
            }
        }
    }
}

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 'vendor/autoload.php';

use Ratchet\Client\Connector;
use React\EventLoop\Loop;
use React\Socket\Connector as SocketConnector;

# API キーが環境変数に設定されていない場合は、次の行を $api_key="your_api_key" に置き換えてください。本番コードでは API キーを直接ハードコーディングすることは、API キー漏洩のリスクを減らすため推奨されません。
$api_key = getenv("DASHSCOPE_API_KEY");
$websocket_url = 'wss://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference/'; // WebSocket サーバーアドレス
$output_file = 'output.mp3'; // 出力ファイルパス

$loop = Loop::get();

if (file_exists($output_file)) {
    // ファイル内容をクリア
    file_put_contents($output_file, '');
    echo "File cleared\n";
}

// カスタムコネクタを作成
$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'
];

// WebSocket サービスに接続
$connector($websocket_url, [], $headers)
    ->then(function ($conn) use ($output_file) {
        echo "Connection established\n";

        // WebSocket メッセージを非同期で受信
        $conn->on('message', function ($msg) use ($conn, $output_file) {
            if ($msg->isBinary()) {
                // バイナリデータをローカルファイルに書き込む
                file_put_contents($output_file, $msg->getPayload(), FILE_APPEND);
                echo "Binary data written to file\n";
            } else {
                $data = json_decode($msg, true);
                switch ($data['header']['event']) {
                    case 'task-started':
                        echo "Task started\n";
                        break;
                    case 'result-generated':
                        // 追加のメッセージを取得する必要がある場合は、ここにコードを追加します
                        break;
                    case 'task-finished':
                        echo "Task completed\n";
                        $conn->close();
                        break;
                    case 'task-failed':
                        echo "Task failed: " . $data['header']['error_message'] . "\n";
                        $conn->close();
                        break;
                    default:
                        echo "Unknown event: " . $msg . "\n";
                }
            }
        });

        // 接続クローズをリッスン
        $conn->on('close', function($code = null, $reason = null) {
            echo "Connection closed\n";
            if ($code !== null) {
                echo "Close code: " . $code . "\n";
            }
            if ($reason !== null) {
                echo "Close reason: " . $reason . "\n";
            }
        });

        // run-task コマンドを送信
        $conn->send(json_encode([
            'header' => [
                'action' => 'run-task',
                'task_id' => bin2hex(random_bytes(16)),
                'streaming' => 'out'
            ],
            'payload' => [
                'model' => 'sambert-zhichu-v1',
                'task_group' => 'audio',
                'task' => 'tts',
                'function' => 'SpeechSynthesizer',
                'input' => [
                    'text' => 'Bright moonlight before my bed, I wonder if it is frost on the ground. I raise my head to watch the bright moon, and lower my head to think of my hometown.'
                ],
                'parameters' => [
                    'text_type' => 'PlainText',
                    'format' => 'mp3',
                    'sample_rate' => 16000,
                    'volume' => 50,
                    'rate' => 1,
                    'pitch' => 1,
                    'word_timestamp_enabled' => true,
                    'phoneme_timestamp_enabled' => true
                ]
            ]
        ]));
        echo "run-task command sent\n";
    }, function (Exception $e) {
        echo "Connection failed: {$e->getMessage()}\n";
        file_put_contents('error.log', $e->getMessage() . "\n", FILE_APPEND);
    });

$loop->run();

Node.js

必要な依存関係をインストールします:

npm install ws
npm install uuid

サンプルコード:

const WebSocket = require('ws');
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');

// API キーが環境変数に設定されていない場合は、次の行を apiKey = 'your_api_key' に置き換えてください。本番コードでは API キーを直接ハードコーディングすることは、API キー漏洩のリスクを減らすため推奨されません。
const apiKey = process.env.DASHSCOPE_API_KEY;
const wsUrl = 'wss://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference/'; // WebSocket サーバーアドレス
const outputFilePath = 'output.mp3'; // 音声ファイルのパスに置き換えてください

async function main() {
  await checkAndClearOutputFile(outputFilePath);
  createWebSocketConnection();
}

const fileStream = fs.createWriteStream(outputFilePath, { flags: 'a' });
function createWebSocketConnection() {
  const ws = new WebSocket(wsUrl, {
    headers: {
      Authorization: `bearer ${apiKey}`,
      'X-DashScope-DataInspection': 'enable'
    }
  });

  ws.on('open', () => {
    console.log('Connected to WebSocket server');
    sendRunTaskMessage(ws);
  });

  ws.on('message', (data, isBinary) => handleWebSocketMessage(data, isBinary, ws));
  ws.on('error', (error) => console.error('WebSocket error:', error));
  ws.on('close', () => console.log('WebSocket connection closed'));

  return ws;
}

function sendRunTaskMessage(ws) {
  const taskId = uuidv4();
  const runTaskMessage = {
    header: {
      action: 'run-task',
      task_id: taskId,
      streaming: 'out'
    },
    payload: {
      model: 'sambert-zhichu-v1',
      task_group: 'audio',
      task: 'tts',
      function: 'SpeechSynthesizer',
      input: {
        text: 'The white sun sets behind the mountains, and the Yellow River flows into the sea. To see a thousand miles further, you must climb one more story.'
      },
      parameters: {
        text_type: 'PlainText',
        format: 'mp3',
        sample_rate: 16000,
        volume: 50,
        rate: 1,
        pitch: 1,
        word_timestamp_enabled: true,
        phoneme_timestamp_enabled: true
      }
    }
  };
  ws.send(JSON.stringify(runTaskMessage));
  console.log('run-task command sent');
}

function handleWebSocketMessage(data, isBinary, ws) {
  if (isBinary) {
    fileStream.write(data);
  } else {
    const message = JSON.parse(data);
    handleWebSocketEvent(message, ws);
  }
}

function handleWebSocketEvent(message, ws) {
  switch (message.header.event) {
    case 'task-started':
      console.log('Task started');
      break;
    case 'result-generated':
      console.log('Result generated');
      break;
    case 'task-finished':
      console.log('Task completed');
      ws.close();
      fileStream.end(() => {
        console.log('File stream closed');
      });
      break;
    case 'task-failed':
      console.error('Task failed:', message.header.error_message);
      ws.close();
      fileStream.end(() => {
        console.log('File stream closed');
      });
      break;
    default:
      console.log('Unknown event:', message.header.event);
  }
}

function checkAndClearOutputFile(filePath) {
  return new Promise((resolve, reject) => {
    fs.access(filePath, fs.F_OK, (err) => {
      if (!err) {
        fs.truncate(filePath, 0, (truncateErr) => {
          if (truncateErr) return reject(truncateErr);
          console.log('File cleared');
          resolve();
        });
      } else {
        fs.open(filePath, 'w', (openErr) => {
          if (openErr) return reject(openErr);
          console.log('File created');
          resolve();
        });
      }
    });
  });
}

main().catch(console.error);

本番デプロイメント

接続の再利用 (WebSocket)

WebSocket 接続は再利用をサポートしています:合成タスクが完了した後、再接続せずに同じ接続で次のタスクを開始できます。

再利用フロー

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

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

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

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

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

  4. 60 秒以内に新しいタスクが開始されない場合、接続は自動的に切断されます。

イベントの詳細については、対応するCosyVoice API リファレンスQwen-TTS API リファレンスをご参照ください。

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

DashScope SDK には、WebSocket 接続と合成オブジェクトを再利用するための組み込みプーリングメカニズムが含まれており、頻繁な作成と破棄のオーバーヘッドを削減します。

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

CosyVoice

前提条件

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)
    

    SpeechSynthesizerObjectPool は、初期化時に現在のグローバルな dashscope.api_key 値を使用して WebSocket 接続を確立します。API キーは WebSocket ハンドシェイク中にのみ Authorization ヘッダーに書き込まれます。後続のタスクメッセージ (例:run-task) はそれを持ちません。プール作成後に dashscope.api_key を変更しても、既存の接続には影響しません。borrow_synthesizer によって返されるオブジェクト (返却後に再利用されるオブジェクトを含む) は、元のハンドシェイクの API キーを使用し続けます。新しい値は暗黙的に無視され、ID、クォータ、または課金の帰属が期待と異なる可能性があります。borrow_synthesizer は、パラメーターを介して API キーを指定することをサポートしていません。

    複数の異なる 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 = [
    'First sentence: Welcome to Alibaba Cloud speech synthesis.',
    'Second sentence: Welcome to Alibaba Cloud speech synthesis.',
    'Third sentence: Welcome to Alibaba Cloud speech synthesis.',
]
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}] start')

        def on_complete(self):
            print(f'[task_{task_id}] speech synthesis task complete successfully.')
            complete_event.set()

        def on_error(self, message: str):
            print(f'[task_{task_id}] speech synthesis task failed, {message}')

        def on_close(self):
            # オブジェクトプールを使用する場合、on_open はタスク終了後に呼び出されます
            print(f'[task_{task_id}] finished')

        def on_event(self, message):
            # print(f'recv speech synthsis message {message}')
            pass

        def on_data(self, data: bytes) -> None:
            # プレーヤーに送信
            # 音声をファイルに保存
            self.file.write(data)

    # 音声合成コールバックを呼び出す
    synthesizer_callback = Callback()

    # 音声合成器を初期化
    # 音声、フォーマット、サンプルレートなどの合成パラメーターをカスタマイズできます
    if USE_CONNECTION_POOL:
        speech_synthesizer = connectionPool.borrow_synthesizer(
            model='cosyvoice-v3-flash',
            voice='longanyang',
            seed=12382,
            callback=synthesizer_callback
        )
    else:
        speech_synthesizer = SpeechSynthesizer(model='cosyvoice-v3-flash',
                                               voice='longanyang',
                                               seed=12382,
                                               callback=synthesizer_callback)
    try:
        speech_synthesizer.call(text)
    except Exception as e:
        print(f'[task_{task_id}] speech synthesis task failed, {e}')
        if USE_CONNECTION_POOL:
            # 接続プールを使用している場合、タスクが失敗した場合は手動で合成器の接続を閉じます。
            speech_synthesizer.close()
        return

    print('[task_{}] Synthesized text: {}'.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 を変更しても、プール内の既存の接続には影響しません。
    # 以下の URL はシンガポールリージョンを指しています。中国 (北京) リージョンのモデルを使用している場合は、wss://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference に置き換えてください
    dashscope.base_websocket_api_url='wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'
    init_dashscope_api_key()
    if USE_CONNECTION_POOL:
        print('creating connection pool')
        start_time = time.time() * 1000
        connectionPool = SpeechSynthesizerObjectPool(max_size=3)
        end_time = time.time() * 1000
        print('connection pool created, cost: {} ms'.format(end_time - start_time))
    task_thread_list = []
    for task_id in range(3):
        thread = threading.Thread(
            target=synthesis_text_to_speech_and_play_by_streaming_mode,
            args=(text_to_synthesize[task_id], task_id))
        task_thread_list.append(thread)

    for task_thread in task_thread_list:
        task_thread.start()

    for task_thread in task_thread_list:
        task_thread.join()

    if USE_CONNECTION_POOL:
        connectionPool.shutdown()

リソース管理とエラー処理

  • タスク成功:音声合成タスクが正常に完了した場合、connectionPool.return_synthesizer(speech_synthesizer) を呼び出して SpeechSynthesizer オブジェクトをプールに返却し、再利用します。

    重要

    未完了または失敗したタスクの SpeechSynthesizer オブジェクトは返却しないでください。

  • タスク失敗:SDK 内部エラーまたはビジネスロジック例外によりタスクが中断された場合、基盤となる WebSocket 接続を閉じます:speech_synthesizer.close()

  • すべての音声合成タスクが完了したら、オブジェクトプールをシャットダウンします:connectionPool.shutdown()

  • サービスが TaskFailed エラーを返した場合、追加の処理は必要ありません。

Java SDK

Java SDK は、組み込みの接続プールとカスタムオブジェクトプールが連携して最適なパフォーマンスを実現します。

  • 接続プール:SDK に統合された OkHttp3 接続プールは、基盤となる WebSocket 接続を管理および再利用して、ネットワークハンドシェイクのオーバーヘッドを削減します。この機能はデフォルトで有効になっています。

  • オブジェクトプール:commons-pool2 に基づいて実装され、事前に確立された接続を持つ SpeechSynthesizer オブジェクトのグループを維持します。プールからオブジェクトを取得すると、接続確立のレイテンシーがなくなり、初回音声受信までの時間が大幅に短縮されます。

実装手順

  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 は、コールバックとタスクレベルのパラメーター (voiceformat など) を更新するために、借用ごとに 1 回呼び出されます。各呼び出しで渡される apiKey は同じでなければなりません。updateParamAndCallback は、現在の SpeechSynthesizer インスタンスのローカルフィールドのみを更新し、基盤となる WebSocket 接続を再構築しません。SDK は、WebSocket ハンドシェイク中にのみ apiKeyAuthorization ヘッダーに書き込みます。後続のタスクメッセージ (例:run-task) はそれを持ちません。再利用された接続は開いたままなので、異なる apiKey がサーバーに送信されることはありません。リクエストは元のハンドシェイクの apiKey を使用し続け、ID、クォータ、または課金の帰属が期待と異なる可能性があります。

    複数の異なる API キーを使用する必要がある場合は、各 API キーに対して別々のオブジェクトプールインスタンスを維持してください。

  6. SpeechSynthesizer オブジェクトを返却する

    音声合成タスクが完了したら、SpeechSynthesizer オブジェクトを返却して、後続のタスクが再利用できるようにします。

    タスクが未完了または失敗したオブジェクトは返却しないでください。

    CosyvoiceObjectPool.getInstance().returnObject(synthesizer);
    
完全なコード

オブジェクトプールのシナリオでは、各 updateParamAndCallback 呼び出しに渡される apiKey同じでなければなりません。SDK は確立された接続の apiKey を更新しません。異なる apiKey を渡しても効果はありません。複数のキーを使用するシナリオでは、API キーごとに別々のオブジェクトプールインスタンスを維持してください。詳細については、「音声合成の実行」セクションの重要な注意をご参照ください。

import com.alibaba.dashscope.audio.tts.SpeechSynthesisResult;
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesisAudioFormat;
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesisParam;
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesizer;
import com.alibaba.dashscope.common.ResultCallback;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.alibaba.dashscope.utils.Constants;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;

/**
 * プロジェクトに org.apache.commons.pool2 と DashScope 関連のパッケージをインポートする必要があります。
 *
 * DashScope SDK 2.16.6 以降のバージョンは、高い同時実行性シナリオ向けに最適化されています。
 * DashScope SDK 2.16.6 より前のバージョンは、高い同時実行性シナリオには推奨されません。
 *
 *
 * TTS サービスへの高い同時実行性呼び出しを行う前に、
 * 次の環境変数を使用して接続プールパラメーターを設定してください。
 *
 * DASHSCOPE_MAXIMUM_ASYNC_REQUESTS
 * DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST
 * DASHSCOPE_CONNECTION_POOL_SIZE
 *
 */

class SpeechSynthesizerObjectFactory
        extends BasePooledObjectFactory<SpeechSynthesizer> {
    public SpeechSynthesizerObjectFactory() {
        super();
    }
    @Override
    public SpeechSynthesizer create() throws Exception {
        return new SpeechSynthesizer();
    }

    @Override
    public PooledObject<SpeechSynthesizer> wrap(SpeechSynthesizer obj) {
        return new DefaultPooledObject<>(obj);
    }
}

class CosyvoiceObjectPool {
    public static GenericObjectPool<SpeechSynthesizer> synthesizerPool;
    public static String COSYVOICE_OBJECTPOOL_SIZE_ENV = "COSYVOICE_OBJECTPOOL_SIZE";
    public static int DEFAULT_OBJECT_POOL_SIZE = 500;
    private static Lock lock = new java.util.concurrent.locks.ReentrantLock();
    public static int getObjectivePoolSize() {
        try {
            Integer n = Integer.parseInt(System.getenv(COSYVOICE_OBJECTPOOL_SIZE_ENV));
            System.out.println("Using Object Pool Size In Env: "+ n);
            return n;
        } catch (NumberFormatException e) {
            System.out.println("Using Default Object Pool Size: "+ DEFAULT_OBJECT_POOL_SIZE);
            return DEFAULT_OBJECT_POOL_SIZE;
        }
    }
    public static GenericObjectPool<SpeechSynthesizer> getInstance() {
        lock.lock();
        if (synthesizerPool == null) {
            // ここでオブジェクトプールのサイズを設定するか、COSYVOICE_OBJECTPOOL_SIZE 環境変数で設定できます。
            // この値は、サーバーごとの最大同時実行数の 1.5〜2 倍に設定することを推奨します。
            int objectPoolSize = getObjectivePoolSize();
            SpeechSynthesizerObjectFactory speechSynthesizerObjectFactory =
                    new SpeechSynthesizerObjectFactory();
            GenericObjectPoolConfig<SpeechSynthesizer> config =
                    new GenericObjectPoolConfig<>();
            config.setMaxTotal(objectPoolSize);
            config.setMaxIdle(objectPoolSize);
            config.setMinIdle(objectPoolSize);
            synthesizerPool =
                    new GenericObjectPool<>(speechSynthesizerObjectFactory, config);
        }
        lock.unlock();
        return synthesizerPool;
    }
}

class SynthesizeTaskWithCallback implements Runnable {
    String[] textArray;
    String requestId;
    long timeCost;
    public SynthesizeTaskWithCallback(String[] textArray) {
        this.textArray = textArray;
    }
    @Override
    public void run() {
        SpeechSynthesizer synthesizer = null;
        long startTime = System.currentTimeMillis();
        // onError を受信した場合
        final boolean[] hasError = {false};
        try {
            class ReactCallback extends ResultCallback<SpeechSynthesisResult> {
                ReactCallback() {}

                @Override
                public void onEvent(SpeechSynthesisResult message) {
                    if (message.getAudioFrame() != null) {
                        try {
                            byte[] bytesArray = message.getAudioFrame().array();
                            System.out.println("Audio received. Audio stream length: " + bytesArray.length);
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                    }
                }

                @Override
                public void onComplete() {}

                @Override
                public void onError(Exception e) {
                    System.out.println(e.getMessage());
                    e.printStackTrace();
                    hasError[0] = true;
                }
            }

            SpeechSynthesisParam param =
                    SpeechSynthesisParam.builder()
                            .model("cosyvoice-v3-flash")
                            .voice("longanyang")
                            // シンガポールと北京リージョンの API キーは異なります。API キーを取得するには、https://www.alibabacloud.com/help/model-studio/get-api-key をご参照ください
                            // 環境変数を設定していない場合は、次の行を Model Studio API キーに置き換えてください:.apiKey("sk-xxx")
                            .apiKey(System.getenv("DASHSCOPE_API_KEY"))
                            .format(SpeechSynthesisAudioFormat
                                    .MP3_22050HZ_MONO_256KBPS) // ストリーミング合成には PCM または MP3 を使用
                            .build();

            try {
                synthesizer = CosyvoiceObjectPool.getInstance().borrowObject();
                // 重要:オブジェクトプールのシナリオでは、各 updateParamAndCallback 呼び出しに渡される apiKey は同じでなければなりません。SDK は確立された接続の apiKey を更新しません。異なる apiKey を渡しても効果はありません。「音声合成の実行」セクションの詳細をご参照ください。
                synthesizer.updateParamAndCallback(param, new ReactCallback());
                for (String text : textArray) {
                    synthesizer.streamingCall(text);
                }
                Thread.sleep(20);
                synthesizer.streamingComplete(60000);
                requestId = synthesizer.getLastRequestId();
            } catch (Exception e) {
                System.out.println("Exception e: " + e.toString());
                hasError[0] = true;
            }
        } catch (Exception e) {
            hasError[0] = true;
            throw new RuntimeException(e);
        }
        if (synthesizer != null) {
            try {
                if (hasError[0] == true) {
                    // エラーが発生した場合、接続を閉じ、オブジェクトプール内のオブジェクトを無効にします。
                    synthesizer.getDuplexApi().close(1000, "bye");
                    CosyvoiceObjectPool.getInstance().invalidateObject(synthesizer);
                } else {
                    // タスクが正常に完了した場合、オブジェクトをプールに返却します。
                    CosyvoiceObjectPool.getInstance().returnObject(synthesizer);
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            long endTime = System.currentTimeMillis();
            timeCost = endTime - startTime;
            System.out.println("[Thread " + Thread.currentThread() + "] Speech synthesis task completed. Time elapsed: " + timeCost + " ms, RequestId " + requestId);
        }
    }
}

@Slf4j
public class SynthesizeTextToSpeechWithCallbackConcurrently {
    public static void checkoutEnv(String envName, int defaultSize) {
        if (System.getenv(envName) != null) {
            System.out.println("[ENV CHECK]: " + envName + " "
                    + System.getenv(envName));
        } else {
            System.out.println("[ENV CHECK]: " + envName
                    + " Using Default which is " + defaultSize);
        }
    }

    public static void main(String[] args)
            throws InterruptedException, NoApiKeyException {
        // シンガポールリージョンの URL。WorkspaceId を実際のワークスペース ID に置き換えてください。URL はリージョンによって異なります。
        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[] {
                    "Before my bed, moonlight gleams,", "It seems like frost upon the ground.", "I lift my gaze to watch the bright moon,", "Then bow my head, thinking of home."}));
        }

        // ExecutorService をシャットダウンし、すべてのタスクが完了するのを待つ
        executorService.shutdown();
        executorService.awaitTermination(1, TimeUnit.MINUTES);
        System.exit(0);
    }
}

推奨構成

以下の構成は、Alibaba Cloud サーバーで指定された仕様で CosyVoice 音声合成サービスのみを実行したテスト結果に基づいています。過度に高い同時実行性は、タスク処理の遅延を引き起こす可能性があります。

ここで、サーバーごとの同時実行数とは、特定の瞬間に同時に実行されている CosyVoice 音声合成タスクの数を指し、ワーカースレッド数とも理解できます。

サーバー仕様 (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 内部またはビジネスロジックがタスクを中断する例外をスローした場合、次の 2 つの操作を実行する必要があります:

    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 側のレイテンシーと実際の初回音声受信までの時間の違い

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 秒後に接続タイムアウトをトリガーするまで開いたままになります。これは、その 61 秒間、接続が再利用できないことを意味します。

高い同時実行性シナリオでは、再利用可能な接続がない場合、新しいタスクは新しい接続を作成し、次の結果につながります:

  1. 接続数が増加し続けます。

  2. 過剰な接続がサーバーリソースを使い果たし、サーバーが応答しなくなります。

  3. 接続プールが上限に達し、新しいタスクは利用可能な接続を待ってブロックされます。

タイプ 2:

オブジェクトプールの設定で MaxIdle が MaxTotal より小さい値に設定されている場合、MaxIdle を超えるアイドルオブジェクトは破棄され、接続リークが発生します。リークした接続は、61 秒のタイムアウトが切断をトリガーするまで開いたままになります。タイプ 1 と同様に、これにより接続数が増え続けます。

解決策

タイプ 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. 実際の同時実行レベルが高すぎないか確認します。

Sambert

Sambert は、Java SDK でのみ組み込みのプーリングをサポートしています。Python SDK はプーリングをサポートしていません。

前提条件

推奨構成

接続プールとオブジェクトプールのサイズは、「大きいほど良い」というわけではありません。プールサイズが小さすぎても大きすぎても、パフォーマンスが低下する可能性があります。ご利用のサーバーの実際の仕様に基づいて設定してください。

以下の推奨構成は、サーバー上で Sambert 音声合成サービスのみを実行してテストした結果に基づいています:

マシン構成 (Alibaba Cloud)

サーバーあたりの最大同時実行数

オブジェクトプールサイズ

接続プールサイズ

4 vCPU、8 GiB

600

1200

2000

サーバーあたりの最大同時実行数とは、同時に実行される Sambert 音声合成タスクの数を指し、ワーカースレッド数に相当します。
重要

高同時実行数の呼び出しでは、同じオブジェクトが同じ WebSocket 接続を再利用するため、WebSocket 接続はサービス起動時にのみ作成されます。

同時に作成される WebSocket 接続が多すぎると、ブロッキングが発生します。サービスの起動時には、サーバーあたりの同時実行数を徐々に増やしてください。

設定可能なパラメーター

接続プール

DashScope Java SDK は、OkHttp3 接続プールを使用して WebSocket 接続を再利用し、頻繁な接続作成によるオーバーヘッドとレイテンシーを削減します。

接続プールは、DashScope SDK でデフォルトで有効になっています。ユースケースに基づいてプールサイズを設定してください。

Java サービスを実行する前に、環境変数を通じて接続プールのパラメーターを設定します。接続プールの設定パラメーターは次のとおりです:

DASHSCOPE_CONNECTION_POOL_SIZE

接続プールのサイズ。デフォルト値:32。

推奨値:ピーク時の同時実行数の 2 倍以上。

DASHSCOPE_MAXIMUM_ASYNC_REQUESTS

非同期リクエストの最大数。デフォルト値:32。

推奨値:接続プールのサイズと同じ。

詳細については、リファレンスドキュメントをご参照ください。

DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST

ホストあたりの非同期リクエストの最大数。デフォルト値:32。

推奨値:接続プールのサイズと同じ。

詳細については、リファレンスドキュメントをご参照ください。

オブジェクトプール

オブジェクトプールを使用して SpeechSynthesizer オブジェクトを再利用します。これにより、オブジェクトの繰り返し作成と破棄によるメモリと時間のオーバーヘッドをさらに削減できます。

Java サービスを実行する前に、環境変数またはコードを通じてオブジェクトプールのサイズを設定します。オブジェクトプールの設定パラメーターは次のとおりです:

SAMBERT_OBJECTPOOL_SIZE

オブジェクトプールのサイズ。

推奨値:ピーク時の同時実行数の 1.5〜2 倍。

オブジェクトプールのサイズは、接続プールのサイズ以下である必要があります。そうしないと、接続を待機しているオブジェクトが呼び出しのブロッキングを引き起こします。

環境変数の設定の詳細については、「API キーを環境変数として設定」をご参照ください。

サンプルコード

次の例では、リソースプールを使用します。オブジェクトプールはグローバルシングルトンです。

  • 各プライマリアカウントは、デフォルトで 1 秒あたり最大 3 つの Sambert 音声合成タスクを送信できます。

    より高い QPS をリクエストするには、お問い合わせください。

  • DashScope および org.apache.commons.pool2 パッケージをインポートします。DashScope はバージョン 2.16.9 以降が必要です。

    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. Maven コマンド ( mvn clean install mvn compile など) を実行して、プロジェクトの依存関係を更新します。

    Gradle

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

    2. dependencies ブロック内に次の依存関係を追加します。

      dependencies {
          // 'the-latest-version' をバージョン 2.16.9 以降に置き換えます。利用可能なバージョンは 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
      
      説明

      プロジェクトに Gradle Wrapper ファイル ( gradlew または gradlew.bat ) がない場合は、次のいずれかの方法で対応できます:

      • インストール済みの Gradle で gradle build --refresh-dependencies を直接実行します。

      • または、最初に gradle wrapper を実行してラッパーファイルを生成してから、上記のコマンドを実行します。

  • サンプルコードでは、異なるスレッドがランダムな時間待機することで、同時に多数の WebSocket 接続が作成されるのを回避します。

import com.alibaba.dashscope.audio.tts.SpeechSynthesisAudioFormat;
import com.alibaba.dashscope.audio.tts.SpeechSynthesisParam;
import com.alibaba.dashscope.audio.tts.SpeechSynthesisResult;
import com.alibaba.dashscope.audio.tts.SpeechSynthesizer;
import com.alibaba.dashscope.common.ResultCallback;
import com.alibaba.dashscope.exception.NoApiKeyException;
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.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;

/**
 * TTS サービスを高同時実行数で呼び出す前に、
 * 次の環境変数を通じて接続プールのサイズを設定してください。
 *
 * DASHSCOPE_MAXIMUM_ASYNC_REQUESTS=2000
 * DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST=2000
 * DASHSCOPE_CONNECTION_POOL_SIZE=2000
 *
 * デフォルトは 32 です。単一サーバーの最大同時接続数の 2 倍に設定することを推奨します。
 */

@Slf4j
public class SynthesizeTextToSpeechUsingSambertConcurrently {
    public static void checkoutEnv(String envName, int defaultSize) {
        if (System.getenv(envName) != null) {
            System.out.println("[ENV CHECK]: " + envName + " "
                    + System.getenv(envName));
        } else {
            System.out.println("[ENV CHECK]: " + envName
                    + " Using Default which is " + defaultSize);
        }
    }

    public static void main(String[] args)
            throws InterruptedException, NoApiKeyException {

        // 接続プール環境のチェック
        checkoutEnv("DASHSCOPE_CONNECTION_POOL_SIZE", 32);
        checkoutEnv("DASHSCOPE_MAXIMUM_ASYNC_REQUESTS", 32);
        checkoutEnv(SambertObjectPool.SAMBERT_OBJECTPOOL_SIZE_ENV, SambertObjectPool.DEFAULT_CONNECTION_POOL_SIZE);
        checkoutEnv("DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST", 32);

        // タスク開始時刻の記録
        int runTimes = 1;

        // SpeechSynthesis オブジェクトのプールを作成
        ExecutorService executorService = Executors.newFixedThreadPool(runTimes);

        for (int i = 0; i < runTimes; i++) {
            executorService.submit(new SynthesizeTask(new String[]{
                    "Before my bed, moonlight gleams,",
                    "It seems like frost upon the ground.",
                    "I lift my gaze to watch the bright moon,",
                    "Then bow my head, thinking of home."
            }));
        }

        // ExecutorService をシャットダウンし、すべてのタスクが完了するのを待機
        executorService.shutdown();
        executorService.awaitTermination(1, TimeUnit.MINUTES);
        System.exit(0);
    }
}

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 SambertObjectPool {
    public static GenericObjectPool<SpeechSynthesizer> synthesizerPool;
    public static String SAMBERT_OBJECTPOOL_SIZE_ENV = "SAMBERT_OBJECTPOOL_SIZE";
    public static int DEFAULT_CONNECTION_POOL_SIZE = 500;
    private static Lock lock = new java.util.concurrent.locks.ReentrantLock();
    public static int getObjectivePoolSize() {
        try {
            Integer n = Integer.parseInt(System.getenv(SAMBERT_OBJECTPOOL_SIZE_ENV));
            return n;
        } catch (NumberFormatException e) {
            return DEFAULT_CONNECTION_POOL_SIZE;
        }
    }
    public static GenericObjectPool<SpeechSynthesizer> getInstance() {
        lock.lock();
        if (synthesizerPool == null) {
            // ここでオブジェクトプールのサイズを設定できます。または環境変数 SAMBERT_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 SynthesizeTask implements Runnable {
    String[] textList;
    String requestId;
    long timeCost;
    public SynthesizeTask(String[] textList) {
        this.textList = textList;
    }
    @Override
    public void run() {
        // タスク開始前にランダムな時間スリープし、同時に多数の WebSocket を作成するのを回避します。
        Random random = new Random();
        try {
            Thread.sleep(random.nextInt(30*1000));
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        for (String text:textList) {
            SpeechSynthesizer synthesizer = null;
            long startTime = System.currentTimeMillis();

            try {
                CountDownLatch latch = new CountDownLatch(1);
                class ReactCallback extends ResultCallback<SpeechSynthesisResult> {
                    ReactCallback() {}

                    @Override
                    public void onEvent(SpeechSynthesisResult message) {
                        if (message.getAudioFrame() != null) {
                            try {
                                byte[] bytesArray = message.getAudioFrame().array();
                            } catch (Exception e) {
                                throw new RuntimeException(e);
                            }
                        }
                    }

                    @Override
                    public void onComplete() {
                        latch.countDown();
                    }

                    @Override
                    public void onError(Exception e) {
                        System.out.println(e.getMessage());
                        e.printStackTrace();
                        latch.countDown();
                    }
                }

                SpeechSynthesisParam param =
                        SpeechSynthesisParam.builder()
                                .model("sambert-zhichu-v1")
                                .format(SpeechSynthesisAudioFormat.MP3) // PCM または MP3 を使用
                                .text(text)
                                .enablePhonemeTimestamp(true)
                                .enableWordTimestamp(true)
                                // 環境変数を設定していない場合は、次の行を実際の Model Studio API キーに置き換えます: .apiKey("sk-xxx")
                                .apiKey(System.getenv("DASHSCOPE_API_KEY"))
                                .build();

                try {
                    synthesizer = SambertObjectPool.getInstance().borrowObject();
                    synthesizer.call(param, new ReactCallback());
                    try {
                        latch.await();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    requestId = synthesizer.getLastRequestId();
                } catch (Exception e) {
                    System.out.println("Exception e: " + e.toString());
                    synthesizer.getSyncApi().close(1000, "bye");
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                if (synthesizer != null) {
                    try {
                        // SpeechSynthesizer オブジェクトをプールに返却
                        SambertObjectPool.getInstance().returnObject(synthesizer);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
            long endTime = System.currentTimeMillis();
            timeCost = endTime - startTime;
            System.out.println("[スレッド " + Thread.currentThread() + "] 音声合成タスク: (" + text + ") 完了。所要時間: " + timeCost + " ms, RequestId " + requestId);
        }
    }
}
エラー処理
  • サービスが TaskFailed エラーを返した場合、追加の処理は不要です。

  • クライアント側エラー (SDK 内部例外やビジネスロジック例外など) が原因で、合成タスクが完了する前に中断された場合は、手動で接続を閉じる必要があります。

    接続を閉じるには、次のようにします:

    // 次のコードを try-catch ブロックに配置します
    synthesizer.getSyncApi().close(1000, "bye");
    

よくある例外

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

原因:

タイプ 1:

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

高同時実行数のシナリオでは、再利用可能な接続がない場合、新しいタスクが新しい接続を作成するため、次の結果につながります:

  1. 接続数が常に増加し続ける。

  2. 過剰な接続がサーバーリソースを枯渇させ、サーバーが応答しなくなる。

  3. 接続プールが上限に達し、新しいタスクが利用可能な接続を待機してブロックされる。

タイプ 2:

オブジェクトプールの構成で MaxIdle が MaxTotal より小さい値に設定されている場合、MaxIdle を超えるアイドル状態のオブジェクトは破棄され、接続リークが発生します。リークした接続は、61 秒のタイムアウトが切断をトリガーするまで開いたままになります。タイプ 1 と同様に、これにより接続数が常に増加し続けます。

解決策

タイプ 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 キーを選択してください:

  • 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 キーを選択します:

  • 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. ソース音声の品質の確認

    音声クローニングに使用したソース音声が音声要件とベストプラクティスを満たしているか確認します:

    • 音声の長さ:10〜20 秒

    • クリアな音質

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

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

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

Q: クローン音声が不安定な、または不完全な音声を生成する場合、どうすればよいですか?

クローンした音声から合成された音声に次のいずれかの問題が見られる場合:

  • テキストの一部しか読み上げない不完全な再生

  • 合成品質の不一致

  • 音声に異常な間や無音部分がある

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

解決策:ソース音声が音声クローニングの録音ガイドの要件を満たしているか確認してください。録音ガイドラインに基づいて再録音することをお勧めします。

Q:実際の持続時間が WAV ファイルヘッダーに表示される持続時間と異なるのはなぜですか?

音声合成は、データが生成されるにつれて段階的にデータを返すストリーミングメカニズムを使用します。保存された WAV ファイルヘッダーの持続時間は推定値であり、不正確な場合があります。正確な持続時間を得るには、formatpcm に設定し、完全な合成結果を待ってから、自分で 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 キーの範囲を制限します。詳細については、「ワークスペースの管理」をご参照ください。