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

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

最終更新日:Jan 06, 2026

Qwen リアルタイム音声合成モデルは、ストリーミングテキスト入力とオーディオ出力により、低遅延の音声合成を提供します。人間のような様々な音声を提供し、複数の言語や方言に対応し、異なる言語間で一貫した音声を可能にします。また、このモデルは自動的にトーンを調整し、複雑なテキストをスムーズに処理します。

主な特徴

  • リアルタイムで高忠実度の音声を生成し、中国語や英語を含む複数の言語で自然な響きの音声に対応します。

  • 音声クローン (参照オーディオから音声をクローンする) と音声デザイン (テキスト記述から音声を生成する) の 2 つの音声カスタマイズ方法を提供し、カスタム音声を迅速に作成します。

  • リアルタイム対話シナリオでの低遅延応答のために、ストリーミング入出力に対応します。

  • 速度、ピッチ、音量、ビットレートを調整することで、音声パフォーマンスの詳細な制御を可能にします。

  • 主要なオーディオフォーマットと互換性があり、最大 48 kHz のサンプルレートでのオーディオ出力に対応します。

適用範囲

サポートされているモデル:

国際 (シンガポール)

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

  • Qwen3-TTS-VD-Realtime: qwen3-tts-vd-realtime-2025-12-16 (スナップショット)

  • Qwen3-TTS-VC-Realtime: 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 キー を選択してください:

  • Qwen3-TTS-VD-Realtime: qwen3-tts-vd-realtime-2025-12-16 (スナップショット)

  • Qwen3-TTS-VC-Realtime: 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 (スナップショット)

詳細については、「モデル」をご参照ください。

モデルの選択

シナリオ

推奨モデル

理由

注意事項

ブランドイメージ、専用音声、またはシステム音声の拡張のための音声カスタマイズ (テキスト記述に基づく)

qwen3-tts-vd-realtime-2025-12-16

音声デザインに対応しています。オーディオサンプルを必要とせず、テキスト記述からカスタム音声を作成するため、独自のブランド音声をゼロからデザインするのに最適です。

システム音声または音声クローンには対応していません。

ブランドイメージ、専用音声、またはシステム音声の拡張のための音声カスタマイズ (オーディオサンプルに基づく)

qwen3-tts-vc-realtime-2025-11-27

音声クローンに対応しています。実際のオーディオサンプルから音声を迅速にクローンし、人間のようなブランドボイスプリントを作成し、高い忠実度と一貫性を保証します。

音声デザインおよびシステム音声には対応していません。

インテリジェントカスタマーサービスと対話型ボット

qwen3-tts-flash-realtime-2025-11-27

ストリーミング入出力に対応しています。調整可能な速度とピッチにより、自然な対話体験を提供します。マルチフォーマットのオーディオ出力は、さまざまなデバイスに適応します。

システム音声のみ対応しています。音声クローンまたは音声デザインには対応していません。

多言語コンテンツのブロードキャスト

qwen3-tts-flash-realtime-2025-11-27

複数の言語と中国語の方言に対応し、グローバルなコンテンツ配信のニーズに応えます。

システム音声のみ対応しています。音声クローンおよびデザインには対応していません。

オーディオ読み上げとコンテンツ制作

qwen3-tts-flash-realtime-2025-11-27

調整可能な音量、速度、ピッチは、オーディオブックやポッドキャストなどのコンテンツの詳細な制作要件を満たします。

システム音声のみ対応しています。音声クローンも音声デザインも対応していません。

E コマースライブストリーミングと短編動画の吹き替え

qwen3-tts-flash-realtime-2025-11-27

MP3 や Opus などの圧縮フォーマットに対応しており、帯域幅が制限されたシナリオに適しています。調整可能なパラメーターは、さまざまな吹き替えスタイルのニーズに応えます。

システム音声のみ対応しています。音声クローンおよび音声デザインには対応していません。

詳細については、「機能比較」をご参照ください。

クイックスタート

コードを実行する前に、API キーを取得して設定する必要があります。SDK を使用してサービスを呼び出す場合は、最新バージョンの DashScope SDK をインストールする必要もあります。

システム音声を使用した音声合成

以下の例は、システム音声を使用して音声合成を行う方法を示しています。詳細については、「サポートされている音声」をご参照ください。

DashScope SDK の使用

Python

server_commit モード

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


qwen_tts_realtime: QwenTtsRealtime = None
text_to_synthesize = [
    'そうでしょう?こういうスーパーが本当に好きなんです、',
    '特に旧正月の時期は。',
    'スーパーに行くと',
    'なんだか',
    'すごく、すごく幸せな気分になります!',
    'たくさん買いたくなっちゃいます!'
]

DO_VIDEO_TEST = False

def init_dashscope_api_key():
    """
        DashScope API キーを設定します。詳細情報:
        https://github.com/aliyun/alibabacloud-bailian-speech-demo/blob/master/PREREQUISITES.md
    """

    # シンガポールリージョンと北京リージョンでは API キーが異なります。API キーを取得するには、https://www.alibabacloud.com/help/model-studio/get-api-key をご参照ください。
    if 'DASHSCOPE_API_KEY' in os.environ:
        dashscope.api_key = os.environ[
            'DASHSCOPE_API_KEY']  # 環境変数 DASHSCOPE_API_KEY から API キーを読み込みます
    else:
        dashscope.api_key = 'your-dashscope-api-key'  # API キーを手動で設定します



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

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

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

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

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


if __name__  == '__main__':
    init_dashscope_api_key()

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

    callback = MyCallback()

    qwen_tts_realtime = QwenTtsRealtime(
        model='qwen3-tts-flash-realtime',
        callback=callback,
        # 以下はシンガポールリージョンの URL です。北京リージョンのモデルを使用する場合は、URL を wss://dashscope.aliyuncs.com/api-ws/v1/realtime に置き換えてください。
        url='wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime'
        )

    qwen_tts_realtime.connect()
    qwen_tts_realtime.update_session(
        voice = 'Cherry',
        response_format = AudioFormat.PCM_24000HZ_MONO_16BIT,
        mode = 'server_commit'        
    )
    for text_chunk in text_to_synthesize:
        print(f'テキストを送信: {text_chunk}')
        qwen_tts_realtime.append_text(text_chunk)
        time.sleep(0.1)
    qwen_tts_realtime.finish()
    callback.wait_for_finished()
    print('[メトリック] セッション: {}、最初のオーディオ遅延: {}'.format(
                    qwen_tts_realtime.get_session_id(), 
                    qwen_tts_realtime.get_first_audio_delay(),
                    ))

commit モード

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


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

DO_VIDEO_TEST = False

def init_dashscope_api_key():
    """
        DashScope API キーを設定します。詳細情報:
        https://github.com/aliyun/alibabacloud-bailian-speech-demo/blob/master/PREREQUISITES.md
    """

    # シンガポールリージョンと北京リージョンでは API キーが異なります。API キーを取得するには、https://www.alibabacloud.com/help/model-studio/get-api-key をご参照ください。
    if 'DASHSCOPE_API_KEY' in os.environ:
        dashscope.api_key = os.environ[
            'DASHSCOPE_API_KEY']  # 環境変数 DASHSCOPE_API_KEY から API キーを読み込みます
    else:
        dashscope.api_key = 'your-dashscope-api-key'  # API キーを手動で設定します



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

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

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

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

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

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


if __name__  == '__main__':
    init_dashscope_api_key()

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

    callback = MyCallback()

    qwen_tts_realtime = QwenTtsRealtime(
        model='qwen3-tts-flash-realtime',
        callback=callback, 
        # 以下はシンガポールリージョンの URL です。北京リージョンのモデルを使用する場合は、URL を wss://dashscope.aliyuncs.com/api-ws/v1/realtime に置き換えてください。
        url='wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime'
        )

    qwen_tts_realtime.connect()
    qwen_tts_realtime.update_session(
        voice = 'Cherry',
        response_format = AudioFormat.PCM_24000HZ_MONO_16BIT,
        mode = 'commit'        
    )
    print(f'テキストを送信: {text_to_synthesize[0]}')
    qwen_tts_realtime.append_text(text_to_synthesize[0])
    qwen_tts_realtime.commit()
    callback.wait_for_response_done()
    callback.reset_event()
    
    print(f'テキストを送信: {text_to_synthesize[1]}')
    qwen_tts_realtime.append_text(text_to_synthesize[1])
    qwen_tts_realtime.commit()
    callback.wait_for_response_done()
    callback.reset_event()

    print(f'テキストを送信: {text_to_synthesize[2]}')
    qwen_tts_realtime.append_text(text_to_synthesize[2])
    qwen_tts_realtime.commit()
    callback.wait_for_response_done()
    
    qwen_tts_realtime.finish()
    print('[メトリック] セッション: {}、最初のオーディオ遅延: {}'.format(
                    qwen_tts_realtime.get_session_id(), 
                    qwen_tts_realtime.get_first_audio_delay(),
                    ))

Java

サーバーコミットモード

// Dashscope SDK のバージョンは 2.21.16 以降である必要があります。
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.FileNotFoundException;
import java.io.IOException;
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 = {
            "そうでしょう?こういうスーパーが特に好きなんです。",
            "特に旧正月の時期は。",
            "スーパーに行くと。",
            "なんだか気分が。",
            "すごく、すごく幸せになります!",
            "たくさん買いたくなっちゃいます!"
    };

    // リアルタイム 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<>();

        // コンストラクターはオーディオフォーマットとオーディオラインを初期化します。
        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);
                        } 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();
            if (line != null && line.isRunning()) {
                line.drain();
                line.close();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException, LineUnavailableException, FileNotFoundException {
        QwenTtsRealtimeParam param = QwenTtsRealtimeParam.builder()
                .model("qwen3-tts-flash-realtime")
                // 以下はシンガポールリージョンの URL です。中国 (北京) リージョンのモデルを使用する場合は、URL を wss://dashscope.aliyuncs.com/api-ws/v1/realtime に置き換えてください。
                .url("wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime")
                // シンガポールリージョンと中国 (北京) リージョンでは API キーが異なります。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":
                        // セッションが作成されたときのイベントを処理します。
                        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(QwenTtsRealtimeAudioFormat.PCM_24000HZ_MONO_16BIT)
                .mode("server_commit")
                .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 モード

// Dashscope SDK のバージョンは 2.21.16 以降である必要があります。
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.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
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 commit {
    // リアルタイム 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<>();

        // コンストラクターはオーディオフォーマットとオーディオラインを初期化します。
        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);
                        } 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();
            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()
                .model("qwen3-tts-flash-realtime")
                // 以下はシンガポールリージョンの URL です。中国 (北京) リージョンのモデルを使用する場合は、URL を wss://dashscope.aliyuncs.com/api-ws/v1/realtime に置き換えてください。
                .url("wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime")
                // シンガポールリージョンと中国 (北京) リージョンでは API キーが異なります。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() {
//            File file = new File("result_24k.pcm");
//            FileOutputStream fos = new FileOutputStream(file);
            @Override
            public void onOpen() {
                System.out.println("接続が開かれました");
                System.out.println("テキストを入力して Enter キーを押すと送信されます。「quit」と入力するとプログラムを終了します。");
            }
            @Override
            public void onEvent(JsonObject message) {
                String type = message.get("type").getAsString();
                switch(type) {
                    case "session.created":
                        System.out.println("セッションを開始します: " + message.get("session").getAsJsonObject().get("id").getAsString());
                        break;
                    case "response.audio.delta":
                        String recvAudioB64 = message.get("delta").getAsString();
                        byte[] rawAudio = Base64.getDecoder().decode(recvAudioB64);
                        //                            fos.write(rawAudio);
                        // オーディオをリアルタイムで再生します。
                        audioPlayer.write(recvAudioB64);
                        break;
                    case "response.done":
                        System.out.println("応答が完了しました");
                        // オーディオの再生が完了するのを待ちます。
                        try {
                            audioPlayer.waitForComplete();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                        // 次の入力に備えます。
                        completeLatch.get().countDown();
                        break;
                    case "session.finished":
                        System.out.println("セッションが終了しました");
                        if (qwenTtsRef.get() != null) {
                            System.out.println("[メトリック] 応答: " + qwenTtsRef.get().getResponseId() +
                                    ", 最初のオーディオ遅延: " + qwenTtsRef.get().getFirstAudioDelay() + " ms");
                        }
                        completeLatch.get().countDown();
                    default:
                        break;
                }
            }
            @Override
            public void onClose(int code, String reason) {
                System.out.println("接続が閉じられました コード: " + code + ", 理由: " + reason);
                try {
//                    fos.close();
                    // 再生が完了するのを待ってから、プレーヤーをシャットダウンします。
                    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(QwenTtsRealtimeAudioFormat.PCM_24000HZ_MONO_16BIT)
                .mode("commit")
                .build();
        qwenTtsRealtime.updateSession(config);

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

            // ユーザーが 'quit' と入力した場合、プログラムを終了します。
            if ("quit".equalsIgnoreCase(text.trim())) {
                System.out.println("接続を閉じています...");
                qwenTtsRealtime.finish();
                completeLatch.get().await();
                break;
            }

            // ユーザー入力が空の場合、スキップします。
            if (text.trim().isEmpty()) {
                continue;
            }

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

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

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

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

WebSocket API の使用

  1. ランタイム環境の準備

    お使いのオペレーティングシステムに pyaudio をインストールします。

    macOS

    brew install portaudio && pip install pyaudio

    Debian/Ubuntu

    sudo apt-get install python3-pyaudio
    
    または
    
    pip install pyaudio

    CentOS

    sudo yum install -y portaudio portaudio-devel && pip install pyaudio

    Windows

    pip install pyaudio

    インストール後、pip を使用して WebSocket 依存関係をインストールします:

    pip install websocket-client==1.8.0 websockets
  2. クライアントの作成

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

    tts_realtime_client.py

    # -- coding: utf-8 --
    
    import asyncio
    import websockets
    import json
    import base64
    import time
    from typing import Optional, Callable, Dict, Any
    from enum import Enum
    
    
    class SessionMode(Enum):
        SERVER_COMMIT = "server_commit"
        COMMIT = "commit"
    
    
    class TTSRealtimeClient:
        """
        TTS Realtime API と対話するためのクライアント。
    
        このクラスは、TTS Realtime API への接続、テキストデータの送信、オーディオ出力の受信、および WebSocket 接続の管理を行うメソッドを提供します。
    
        Attributes:
            base_url (str):
                Realtime API のベース URL。
            api_key (str):
                ID 検証用の 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,
                "language_type": self.language_type,
                "response_format": "pcm",
                "sample_rate": 24000
            })
    
    
        async def send_event(self, event) -> None:
            """サーバーにイベントを送信します。"""
            event['event_id'] = "event_" + str(int(time.time() * 1000))
            print(f"イベントを送信: type={event['type']}, event_id={event['event_id']}")
            await self.ws.send(json.dumps(event))
    
    
        async def update_session(self, config: Dict[str, Any]) -> None:
            """セッション構成を更新します。"""
            event = {
                "type": "session.update",
                "session": config
            }
            print("セッション構成を更新: ", event)
            await self.send_event(event)
    
    
        async def append_text(self, text: str) -> None:
            """API にテキストデータを送信します。"""
            event = {
                "type": "input_text_buffer.append",
                "text": text
            }
            await self.send_event(event)
    
    
        async def commit_text_buffer(self) -> None:
            """テキストバッファーをコミットして処理をトリガーします。"""
            event = {
                "type": "input_text_buffer.commit"
            }
            await self.send_event(event)
    
    
        async def clear_text_buffer(self) -> None:
            """テキストバッファーをクリアします。"""
            event = {
                "type": "input_text_buffer.clear"
            }
            await self.send_event(event)
    
    
        async def finish_session(self) -> None:
            """セッションを終了します。"""
            event = {
                "type": "session.finish"
            }
            await self.send_event(event)
    
    
        async def wait_for_response_done(self):
            """response.done イベントを待ちます。"""
            if self._response_done_future:
                await self._response_done_future
    
    
        async def handle_messages(self) -> None:
            """サーバーからのメッセージを処理します。"""
            try:
                async for message in self.ws:
                    event = json.loads(message)
                    event_type = event.get("type")
    
                    if event_type != "response.audio.delta":
                        print(f"受信したイベント: {event_type}")
    
                    if event_type == "error":
                        print("エラー: ", event.get('error', {}))
                        continue
                    elif event_type == "session.created":
                        print("セッションが作成されました、ID: ", event.get('session', {}).get('id'))
                    elif event_type == "session.updated":
                        print("セッションが更新されました、ID: ", event.get('session', {}).get('id'))
                    elif event_type == "input_text_buffer.committed":
                        print("テキストバッファーがコミットされました、アイテム ID: ", event.get('item_id'))
                    elif event_type == "input_text_buffer.cleared":
                        print("テキストバッファーがクリアされました。")
                    elif event_type == "response.created":
                        self._current_response_id = event.get("response", {}).get("id")
                        self._is_responding = True
                        # response.done を待つための新しい Future を作成します。
                        self._response_done_future = asyncio.Future()
                        print("応答が作成されました、ID: ", self._current_response_id)
                    elif event_type == "response.output_item.added":
                        self._current_item_id = event.get("item", {}).get("id")
                        print("出力アイテムが追加されました、ID: ", self._current_item_id)
                    # オーディオデルタを処理します。
                    elif event_type == "response.audio.delta" and self.audio_callback:
                        audio_bytes = base64.b64decode(event.get("delta", ""))
                        self.audio_callback(audio_bytes)
                    elif event_type == "response.audio.done":
                        print("オーディオ生成が完了しました。")
                    elif event_type == "response.done":
                        self._is_responding = False
                        self._current_response_id = None
                        self._current_item_id = None
                        # Future を完了としてマークします。
                        if self._response_done_future and not self._response_done_future.done():
                            self._response_done_future.set_result(True)
                        print("応答が完了しました。")
                    elif event_type == "session.finished":
                        print("セッションが終了しました。")
    
            except websockets.exceptions.ConnectionClosed:
                print("接続が閉じられました。")
            except Exception as e:
                print("メッセージ処理中のエラー: ", str(e))
    
    
        async def close(self) -> None:
            """WebSocket 接続を閉じます。"""
            if self.ws:
                await self.ws.close()
  3. 音声合成モードの選択

    Realtime API は以下の 2 つのモードに対応しています:

    • server_commit モード

      クライアントはテキストのみを送信します。サーバーはテキストをどのようにセグメント化し、いつ合成するかをインテリジェントに決定します。このモードは、GPS ナビゲーションなど、合成のタイミングを手動で制御する必要がない低遅延シナリオに適しています。

    • commit モード

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

    server_commit モード

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

    server_commit.py

    import os
    import asyncio
    import logging
    import wave
    from tts_realtime_client import TTSRealtimeClient, SessionMode
    import pyaudio
    
    # QwenTTS サービス構成
    # 以下はシンガポールリージョンの URL です。中国 (北京) リージョンのモデルを使用する場合は、URL を wss://dashscope.aliyuncs.com/api-ws/v1/realtime?model=qwen3-tts-flash-realtime に置き換えてください。
    URL = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime?model=qwen3-tts-flash-realtime"
    # シンガポールリージョンと中国 (北京) リージョンでは API キーが異なります。API キーを取得するには、https://www.alibabacloud.com/help/model-studio/get-api-key をご参照ください。
    # 環境変数を設定していない場合は、次の行を Model Studio API キーに置き換えてください: API_KEY="sk-xxx"
    API_KEY = os.getenv("DASHSCOPE_API_KEY")
    
    if not API_KEY:
        raise ValueError("DASHSCOPE_API_KEY 環境変数を設定してください")
    
    # オーディオデータを収集します。
    _audio_chunks = []
    # リアルタイム再生関連。
    _AUDIO_SAMPLE_RATE = 24000
    _audio_pyaudio = pyaudio.PyAudio()
    _audio_stream = None  # ランタイム時に開かれます。
    
    def _audio_callback(audio_bytes: bytes):
        """TTSRealtimeClient オーディオコールバック: リアルタイムで再生し、キャッシュします。"""
        global _audio_stream
        if _audio_stream is not None:
            try:
                _audio_stream.write(audio_bytes)
            except Exception as exc:
                logging.error(f"PyAudio 再生エラー: {exc}")
        _audio_chunks.append(audio_bytes)
        logging.info(f"受信したオーディオチャンク: {len(audio_bytes)} バイト")
    
    def _save_audio_to_file(filename: str = "output.wav", sample_rate: int = 24000) -> bool:
        """収集したオーディオデータを WAV ファイルに保存します。"""
        if not _audio_chunks:
            logging.warning("保存するオーディオデータがありません")
            return False
    
        try:
            audio_data = b"".join(_audio_chunks)
            with wave.open(filename, 'wb') as wav_file:
                wav_file.setnchannels(1)  # モノラル
                wav_file.setsampwidth(2)  # 16 ビット
                wav_file.setframerate(sample_rate)
                wav_file.writeframes(audio_data)
            logging.info(f"オーディオが保存されました: {filename}")
            return True
        except Exception as exc:
            logging.error(f"オーディオの保存に失敗しました: {exc}")
            return False
    
    async def _produce_text(client: TTSRealtimeClient):
        """テキストの断片をサーバーに送信します。"""
        text_fragments = [
            "Alibaba Cloud Model Studio は、大規模モデル開発とアプリケーション構築のためのワンストッププラットフォームです。",
            "開発者とビジネス担当者の両方が、大規模モデルアプリケーションの設計と構築に深く関わることができます。", 
            "簡単なインターフェイス操作で 5 分で大規模モデルアプリケーションを開発したり、",
            "数時間でカスタムモデルをトレーニングしたりできるため、アプリケーションの革新により集中できます。",
        ]
    
        logging.info("テキストの断片を送信中…")
        for text in text_fragments:
            logging.info(f"断片を送信中: {text}")
            await client.append_text(text)
            await asyncio.sleep(0.1)  # 断片間に短い遅延を追加します。
    
        # セッションを終了する前に、サーバーが内部処理を完了するのを待ちます。
        await asyncio.sleep(1.0)
        await client.finish_session()
    
    async def _run_demo():
        """完全なデモを実行します。"""
        global _audio_stream
        # PyAudio 出力ストリームを開きます。
        _audio_stream = _audio_pyaudio.open(
            format=pyaudio.paInt16,
            channels=1,
            rate=_AUDIO_SAMPLE_RATE,
            output=True,
            frames_per_buffer=1024
        )
    
        client = TTSRealtimeClient(
            base_url=URL,
            api_key=API_KEY,
            voice="Cherry",
            mode=SessionMode.SERVER_COMMIT,
            audio_callback=_audio_callback
        )
    
        # 接続を確立します。
        await client.connect()
    
        # メッセージ処理とテキスト送信を並行して実行します。
        consumer_task = asyncio.create_task(client.handle_messages())
        producer_task = asyncio.create_task(_produce_text(client))
    
        await producer_task  # テキストが送信されるのを待ちます。
    
        # response.done を待ちます。
        await client.wait_for_response_done()
    
        # 接続を閉じ、コンシューマータスクをキャンセルします。
        await client.close()
        consumer_task.cancel()
    
        # オーディオストリームを閉じます。
        if _audio_stream is not None:
            _audio_stream.stop_stream()
            _audio_stream.close()
        _audio_pyaudio.terminate()
    
        # オーディオデータを保存します。
        os.makedirs("outputs", exist_ok=True)
        _save_audio_to_file(os.path.join("outputs", "qwen_tts_output.wav"))
    
    def main():
        """同期エントリポイント。"""
        logging.basicConfig(
            level=logging.INFO,
            format='%(asctime)s [%(levelname)s] %(message)s',
            datefmt='%Y-%m-%d %H:%M:%S'
        )
        logging.info("QwenTTS Realtime Client デモを開始中…")
        asyncio.run(_run_demo())
    
    if __name__ == "__main__":
        main() 

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

    commit モード

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

    commit.py

    import os
    import asyncio
    import logging
    import wave
    from tts_realtime_client import TTSRealtimeClient, SessionMode
    import pyaudio
    
    # QwenTTS サービス構成
    # 以下はシンガポールリージョンの URL です。中国 (北京) リージョンのモデルを使用する場合は、URL を wss://dashscope.aliyuncs.com/api-ws/v1/realtime?model=qwen3-tts-flash-realtime に置き換えてください。
    URL = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime?model=qwen3-tts-flash-realtime"
    # シンガポールリージョンと中国 (北京) リージョンでは API キーが異なります。API キーを取得するには、https://www.alibabacloud.com/help/model-studio/get-api-key をご参照ください。
    # 環境変数を設定していない場合は、次の行を Model Studio API キーに置き換えてください: API_KEY="sk-xxx"
    API_KEY = os.getenv("DASHSCOPE_API_KEY")
    
    if not API_KEY:
        raise ValueError("DASHSCOPE_API_KEY 環境変数を設定してください")
    
    # オーディオデータを収集します。
    _audio_chunks = []
    _AUDIO_SAMPLE_RATE = 24000
    _audio_pyaudio = pyaudio.PyAudio()
    _audio_stream = None
    
    def _audio_callback(audio_bytes: bytes):
        """TTSRealtimeClient オーディオコールバック: リアルタイムで再生し、キャッシュします。"""
        global _audio_stream
        if _audio_stream is not None:
            try:
                _audio_stream.write(audio_bytes)
            except Exception as exc:
                logging.error(f"PyAudio 再生エラー: {exc}")
        _audio_chunks.append(audio_bytes)
        logging.info(f"受信したオーディオチャンク: {len(audio_bytes)} バイト")
    
    def _save_audio_to_file(filename: str = "output.wav", sample_rate: int = 24000) -> bool:
        """収集したオーディオデータを WAV ファイルに保存します。"""
        if not _audio_chunks:
            logging.warning("保存するオーディオデータがありません")
            return False
    
        try:
            audio_data = b"".join(_audio_chunks)
            with wave.open(filename, 'wb') as wav_file:
                wav_file.setnchannels(1)  # モノラル
                wav_file.setsampwidth(2)  # 16 ビット
                wav_file.setframerate(sample_rate)
                wav_file.writeframes(audio_data)
            logging.info(f"オーディオが保存されました: {filename}")
            return True
        except Exception as exc:
            logging.error(f"オーディオの保存に失敗しました: {exc}")
            return False
    
    async def _user_input_loop(client: TTSRealtimeClient):
        """ユーザー入力を継続的に取得し、テキストを送信します。ユーザーが空のテキストを入力すると、コミットイベントを送信し、現在のセッションを終了します。"""
        print("テキストを入力してください。Enter キーを押すとコミットイベントが送信され、現在のセッションが終了します。Ctrl+C または Ctrl+D を押すとプログラムが終了します。")
        
        while True:
            try:
                user_text = input("> ")
                if not user_text:  # ユーザー入力が空です。
                    # 空の入力は会話の終了として扱われます: バッファーをコミット -> セッションを終了 -> ループを中断。
                    logging.info("空の入力です。コミットイベントを送信し、現在のセッションを終了します。")
                    await client.commit_text_buffer()
                    # 早すぎるセッション終了によるオーディオの損失を防ぐため、サーバーがコミットを処理するのを待ちます。
                    await asyncio.sleep(0.3)
                    await client.finish_session()
                    break  # ユーザー入力ループを直接終了します。再度 Enter キーを押す必要はありません。
                else:
                    logging.info(f"テキストを送信中: {user_text}")
                    await client.append_text(user_text)
                    
            except EOFError:  # ユーザーが Ctrl+D を押します。
                break
            except KeyboardInterrupt:  # ユーザーが Ctrl+C を押します。
                break
        
        # セッションを終了します。
        logging.info("セッションを終了しています...")
    async def _run_demo():
        """完全なデモを実行します。"""
        global _audio_stream
        # PyAudio 出力ストリームを開きます。
        _audio_stream = _audio_pyaudio.open(
            format=pyaudio.paInt16,
            channels=1,
            rate=_AUDIO_SAMPLE_RATE,
            output=True,
            frames_per_buffer=1024
        )
    
        client = TTSRealtimeClient(
            base_url=URL,
            api_key=API_KEY,
            voice="Cherry",
            mode=SessionMode.COMMIT,  # COMMIT モードに変更します。
            audio_callback=_audio_callback
        )
    
        # 接続を確立します。
        await client.connect()
    
        # メッセージ処理とユーザー入力を並行して実行します。
        consumer_task = asyncio.create_task(client.handle_messages())
        producer_task = asyncio.create_task(_user_input_loop(client))
    
        await producer_task  # ユーザー入力が完了するのを待ちます。
    
        # response.done を待ちます。
        await client.wait_for_response_done()
    
        # 接続を閉じ、コンシューマータスクをキャンセルします。
        await client.close()
        consumer_task.cancel()
    
        # オーディオストリームを閉じます。
        if _audio_stream is not None:
            _audio_stream.stop_stream()
            _audio_stream.close()
        _audio_pyaudio.terminate()
    
        # オーディオデータを保存します。
        os.makedirs("outputs", exist_ok=True)
        _save_audio_to_file(os.path.join("outputs", "qwen_tts_output.wav"))
    
    def main():
        logging.basicConfig(
            level=logging.INFO,
            format='%(asctime)s [%(levelname)s] %(message)s',
            datefmt='%Y-%m-%d %H:%M:%S'
        )
        logging.info("QwenTTS Realtime Client デモを開始中…")
        asyncio.run(_run_demo())
    
    if __name__ == "__main__":
        main() 

    commit.py を実行します。合成するテキストを複数回入力できます。Realtime API から返されたオーディオを聞くには、空の行で Enter キーを押します。

クローン音声を使用した音声合成

音声クローンサービスはオーディオプレビューを提供しません。クローンされた音声を試聴・評価するには、音声合成に適用する必要があります。

以下の例は、音声クローンによって生成されたカスタム音声を使用して音声合成を行い、元の音声に非常に類似した出力を生成する方法を示しています。この例は、DashScope ソフトウェア開発キット (SDK) の「サーバーコミット」モードのサンプルコードに基づいており、voice パラメーターをカスタムのクローン音声に置き換えています。

  • 重要な原則:音声クローンに使用されるモデル (target_model) は、後続の音声合成に使用されるモデル (model) と同じでなければなりません。そうでない場合、合成は失敗します。

  • この例では、音声クローンにローカルオーディオファイル voice.mp3 を使用します。コードを実行する際には、このファイルをご自身のオーディオファイルに置き換える必要があります。

Python

# coding=utf-8
# pyaudio のインストール手順:
# APPLE Mac OS X
#   brew install portaudio
#   pip install pyaudio
# Debian/Ubuntu
#   sudo apt-get install python-pyaudio python3-pyaudio
#   または
#   pip install pyaudio
# CentOS
#   sudo yum install -y portaudio portaudio-devel && pip install pyaudio
# Microsoft Windows
#   python -m pip install pyaudio

import pyaudio
import os
import requests
import base64
import pathlib
import threading
import time
import dashscope  # DashScope Python SDK のバージョンは 1.23.9 以降である必要があります。
from dashscope.audio.qwen_tts_realtime import QwenTtsRealtime, QwenTtsRealtimeCallback, AudioFormat

# ======= 定数構成 =======
DEFAULT_TARGET_MODEL = "qwen3-tts-vc-realtime-2025-11-27"  # 音声クローンと音声合成には同じモデルを使用する必要があります。
DEFAULT_PREFERRED_NAME = "guanyu"
DEFAULT_AUDIO_MIME_TYPE = "audio/mpeg"
VOICE_FILE_PATH = "voice.mp3"  # 音声クローン用のローカルオーディオファイルの相対パス。

TEXT_TO_SYNTHESIZE = [
    'そうでしょう?こういうスーパーが本当に好きなんです、',
    '特に旧正月の時期は。',
    'スーパーに行くと',
    'なんだか',
    'すごく、すごく幸せな気分になります!',
    'たくさん買いたくなっちゃいます!'
]

def create_voice(file_path: str,
                 target_model: str = DEFAULT_TARGET_MODEL,
                 preferred_name: str = DEFAULT_PREFERRED_NAME,
                 audio_mime_type: str = DEFAULT_AUDIO_MIME_TYPE) -> str:
    """
    音声を作成し、voice パラメーターを返します。
    """
    # シンガポールリージョンと中国 (北京) リージョンでは 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")

    file_path_obj = pathlib.Path(file_path)
    if not file_path_obj.exists():
        raise FileNotFoundError(f"オーディオファイルが見つかりません: {file_path}")

    base64_str = base64.b64encode(file_path_obj.read_bytes()).decode()
    data_uri = f"data:{audio_mime_type};base64,{base64_str}"

    # 以下はシンガポールリージョンの URL です。中国 (北京) リージョンのモデルを使用する場合は、URL を https://dashscope.aliyuncs.com/api/v1/services/audio/tts/customization に置き換えてください。
    url = "https://dashscope-intl.aliyuncs.com/api/v1/services/audio/tts/customization"
    payload = {
        "model": "qwen-voice-enrollment", # この値は変更しないでください。
        "input": {
            "action": "create",
            "target_model": target_model,
            "preferred_name": preferred_name,
            "audio": {"data": data_uri}
        }
    }
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json"
    }

    resp = requests.post(url, json=payload, headers=headers)
    if resp.status_code != 200:
        raise RuntimeError(f"音声の作成に失敗しました: {resp.status_code}, {resp.text}")

    try:
        return resp.json()["output"]["voice"]
    except (KeyError, ValueError) as e:
        raise RuntimeError(f"音声応答の解析に失敗しました: {e}")

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

# ======= コールバッククラス =======
class MyCallback(QwenTtsRealtimeCallback):
    """
    カスタム TTS ストリーミングコールバック。
    """
    def __init__(self):
        self.complete_event = threading.Event()
        self._player = pyaudio.PyAudio()
        self._stream = self._player.open(
            format=pyaudio.paInt16, channels=1, rate=24000, output=True
        )

    def on_open(self) -> None:
        print('[TTS] 接続が確立されました')

    def on_close(self, close_status_code, close_msg) -> None:
        self._stream.stop_stream()
        self._stream.close()
        self._player.terminate()
        print(f'[TTS] 接続が閉じられました code={close_status_code}, msg={close_msg}')

    def on_event(self, response: dict) -> None:
        try:
            event_type = response.get('type', '')
            if event_type == 'session.created':
                print(f'[TTS] セッションが開始されました: {response["session"]["id"]}')
            elif event_type == 'response.audio.delta':
                audio_data = base64.b64decode(response['delta'])
                self._stream.write(audio_data)
            elif event_type == 'response.done':
                print(f'[TTS] 応答が完了しました、応答 ID: {qwen_tts_realtime.get_last_response_id()}')
            elif event_type == 'session.finished':
                print('[TTS] セッションが終了しました')
                self.complete_event.set()
        except Exception as e:
            print(f'[エラー] コールバックイベントの処理に失敗しました: {e}')

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

# ======= メイン実行ロジック =======
if __name__ == '__main__':
    init_dashscope_api_key()
    print('[システム] Qwen TTS Realtime を初期化中 ...')

    callback = MyCallback()
    qwen_tts_realtime = QwenTtsRealtime(
        model=DEFAULT_TARGET_MODEL,
        callback=callback,
        # 以下はシンガポールリージョンの URL です。中国 (北京) リージョンのモデルを使用する場合は、URL を wss://dashscope.aliyuncs.com/api-ws/v1/realtime に置き換えてください。
        url='wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime'
    )
    qwen_tts_realtime.connect()
    
    qwen_tts_realtime.update_session(
        voice=create_voice(VOICE_FILE_PATH), # voice パラメーターを、クローンによって生成されたカスタム音声に置き換えます。
        response_format=AudioFormat.PCM_24000HZ_MONO_16BIT,
        mode='server_commit'
    )

    for text_chunk in TEXT_TO_SYNTHESIZE:
        print(f'[テキストを送信]: {text_chunk}')
        qwen_tts_realtime.append_text(text_chunk)
        time.sleep(0.1)

    qwen_tts_realtime.finish()
    callback.wait_for_finished()

    print(f'[メトリック] session_id={qwen_tts_realtime.get_session_id()}, '
          f'first_audio_delay={qwen_tts_realtime.get_first_audio_delay()}s')

Java

Gson 依存関係をインポートします。Maven または Gradle を使用する場合は、次のように依存関係を追加します:

Maven

pom.xml ファイルに以下の内容を追加します:

<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.13.1</version>
</dependency>

Gradle

build.gradle ファイルに以下の内容を追加します:

// https://mvnrepository.com/artifact/com.google.code.gson/gson
implementation("com.google.code.gson:gson:2.13.1")
import com.alibaba.dashscope.audio.qwen_tts_realtime.*;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.google.gson.Gson;
import com.google.gson.JsonObject;

import javax.sound.sampled.*;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.*;
import java.nio.charset.StandardCharsets;
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 {
    // ===== 定数定義 =====
    // 音声クローンと音声合成には同じモデルを使用する必要があります。
    private static final String TARGET_MODEL = "qwen3-tts-vc-realtime-2025-11-27";
    private static final String PREFERRED_NAME = "guanyu";
    // 音声クローン用のローカルオーディオファイルの相対パス。
    private static final String AUDIO_FILE = "voice.mp3";
    private static final String AUDIO_MIME_TYPE = "audio/mpeg";
    private static String[] textToSynthesize = {
            "そうでしょう?こういうスーパーが本当に好きなんです、",
            "特に旧正月の時期は。",
            "スーパーに行くと",
            "なんだか",
            "すごく、すごく幸せな気分になります!",
            "たくさん買いたくなっちゃいます!"
    };

    // データ URI を生成します。
    public static String toDataUrl(String filePath) throws IOException {
        byte[] bytes = Files.readAllBytes(Paths.get(filePath));
        String encoded = Base64.getEncoder().encodeToString(bytes);
        return "data:" + AUDIO_MIME_TYPE + ";base64," + encoded;
    }

    // API を呼び出して音声を作成します。
    public static String createVoice() throws Exception {
        // シンガポールリージョンと中国 (北京) リージョンでは 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");

        String jsonPayload =
                "{"
                        + "\"model\": \"qwen-voice-enrollment\"," // この値は変更しないでください。
                        + "\"input\": {"
                        +     "\"action\": \"create\","
                        +     "\"target_model\": \"" + TARGET_MODEL + "\","
                        +     "\"preferred_name\": \"" + PREFERRED_NAME + "\","
                        +     "\"audio\": {"
                        +         "\"data\": \"" + toDataUrl(AUDIO_FILE) + "\""
                        +     "}"
                        + "}"
                        + "}";

        HttpURLConnection con = (HttpURLConnection) new URL("https://dashscope.aliyuncs.com/api/v1/services/audio/tts/customization").openConnection();
        con.setRequestMethod("POST");
        con.setRequestProperty("Authorization", "Bearer " + apiKey);
        con.setRequestProperty("Content-Type", "application/json");
        con.setDoOutput(true);

        try (OutputStream os = con.getOutputStream()) {
            os.write(jsonPayload.getBytes(StandardCharsets.UTF_8));
        }

        int status = con.getResponseCode();
        System.out.println("HTTP ステータスコード: " + status);

        try (BufferedReader br = new BufferedReader(
                new InputStreamReader(status >= 200 && status < 300 ? con.getInputStream() : con.getErrorStream(),
                        StandardCharsets.UTF_8))) {
            StringBuilder response = new StringBuilder();
            String line;
            while ((line = br.readLine()) != null) {
                response.append(line);
            }
            System.out.println("応答内容: " + response);

            if (status == 200) {
                JsonObject jsonObj = new Gson().fromJson(response.toString(), JsonObject.class);
                return jsonObj.getAsJsonObject("output").get("voice").getAsString();
            }
            throw new IOException("音声の作成に失敗しました: " + status + " - " + response);
        }
    }

    // リアルタイム 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<>();

        // コンストラクターはオーディオフォーマットとオーディオラインを初期化します。
        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);
                        } 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();
            if (line != null && line.isRunning()) {
                line.drain();
                line.close();
            }
        }
    }

    public static void main(String[] args) throws Exception {
        QwenTtsRealtimeParam param = QwenTtsRealtimeParam.builder()
                .model(TARGET_MODEL)
                // 以下はシンガポールリージョンの URL です。中国 (北京) リージョンのモデルを使用する場合は、URL を wss://dashscope.aliyuncs.com/api-ws/v1/realtime に置き換えてください。
                .url("wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime")
                // シンガポールリージョンと中国 (北京) リージョンでは API キーが異なります。API キーを取得するには、https://www.alibabacloud.com/help/model-studio/get-api-key/ をご参照ください。
                // 環境変数を設定していない場合は、次の行を Model Studio API キーに置き換えてください: .apikey("sk-xxx")
                .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":
                        // セッション作成を処理します。
                        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(createVoice()) // voice パラメーターを、クローンによって生成されたカスタム音声に置き換えます。
                .responseFormat(QwenTtsRealtimeAudioFormat.PCM_24000HZ_MONO_16BIT)
                .mode("server_commit")
                .build();
        qwenTtsRealtime.updateSession(config);
        for (String text:textToSynthesize) {
            qwenTtsRealtime.appendText(text);
            Thread.sleep(100);
        }
        qwenTtsRealtime.finish();
        completeLatch.get().await();

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

デザイン音声を使用した音声合成

音声デザイン機能を使用すると、サービスはプレビューオーディオデータを返します。プレビューオーディオを聴いてニーズを満たしていることを確認してから、音声合成に使用できます。この方法は、呼び出しコストの削減に役立ちます。

  1. カスタム音声を生成し、プレビューを聴きます。満足のいくものであれば次に進みます。そうでなければ、音声を再生成します。

    Python

    import requests
    import base64
    import os
    
    def create_voice_and_play():
        # シンガポールリージョンと北京リージョンでは API キーが異なります。API キーを取得するには、https://www.alibabacloud.com/help/zh/model-studio/get-api-key をご参照ください。
        # 環境変数を設定していない場合は、次の行を置き換えてください: api_key = "sk-xxx"
        api_key = os.getenv("DASHSCOPE_API_KEY")
        
        if not api_key:
            print("エラー: DASHSCOPE_API_KEY 環境変数が見つかりません。API キーを設定してください。")
            return None, None, None
        
        # リクエストデータを準備します
        headers = {
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json"
        }
        
        data = {
            "model": "qwen-voice-design",
            "input": {
                "action": "create",
                "target_model": "qwen3-tts-vd-realtime-2025-12-16",
                "voice_prompt": "落ち着いた中年の男性アナウンサーで、深く豊かで魅力的な声、安定した話し方、明瞭な発音で、ニュース放送やドキュメンタリーのナレーションに適しています。",
                "preview_text": "リスナーの皆様、こんにちは。イブニングニュースへようこそ。",
                "preferred_name": "announcer",
                "language": "en"
            },
            "parameters": {
                "sample_rate": 24000,
                "response_format": "wav"
            }
        }
        
        # シンガポールリージョンの URL。北京リージョンの場合は、https://dashscope.aliyuncs.com/api/v1/services/audio/tts/customization を使用してください。
        url = "https://dashscope-intl.aliyuncs.com/api/v1/services/audio/tts/customization"
        
        try:
            # リクエストを送信します
            response = requests.post(
                url,
                headers=headers,
                json=data,
                timeout=60  # タイムアウト設定を追加します
            )
            
            if response.status_code == 200:
                result = response.json()
                
                # 音声名を取得します
                voice_name = result["output"]["voice"]
                print(f"音声名: {voice_name}")
                
                # プレビューオーディオデータを取得します
                base64_audio = result["output"]["preview_audio"]["data"]
                
                # Base64 オーディオデータをデコードします
                audio_bytes = base64.b64decode(base64_audio)
                
                # オーディオファイルをローカルに保存します
                filename = f"{voice_name}_preview.wav"
                
                # オーディオデータをローカルファイルに書き込みます
                with open(filename, 'wb') as f:
                    f.write(audio_bytes)
                
                print(f"オーディオがローカルファイルに保存されました: {filename}")
                print(f"ファイルパス: {os.path.abspath(filename)}")
                
                return voice_name, audio_bytes, filename
            else:
                print(f"リクエストに失敗しました。ステータスコード: {response.status_code}")
                print(f"応答: {response.text}")
                return None, None, None
                
        except requests.exceptions.RequestException as e:
            print(f"ネットワークリクエストエラー: {e}")
            return None, None, None
        except KeyError as e:
            print(f"応答フォーマットエラー: 必須フィールドがありません: {e}")
            print(f"応答: {response.text if 'response' in locals() else '応答なし'}")
            return None, None, None
        except Exception as e:
            print(f"予期せぬエラー: {e}")
            return None, None, None
    
    if __name__ == "__main__":
        print("音声を作成中...")
        voice_name, audio_data, saved_filename = create_voice_and_play()
        
        if voice_name:
            print(f"\n音声 '{voice_name}' の作成に成功しました")
            print(f"オーディオファイルが保存されました: '{saved_filename}'")
            print(f"ファイルサイズ: {os.path.getsize(saved_filename)} バイト")
        else:
            print("\n音声の作成に失敗しました")

    Java

    Gson 依存関係をインポートする必要があります。Maven または Gradle を使用している場合は、依存関係を追加してください:

    Maven

    pom.xml に以下の内容を追加します:

    <!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
    <dependency>
        <groupId>com.google.code.gson</groupId>
        <artifactId>gson</artifactId>
        <version>2.13.1</version>
    </dependency>

    Gradle

    build.gradle に以下の内容を追加します:

    // https://mvnrepository.com/artifact/com.google.code.gson/gson
    implementation("com.google.code.gson:gson:2.13.1")
    import com.google.gson.JsonObject;
    import com.google.gson.JsonParser;
    import java.io.*;
    import java.net.HttpURLConnection;
    import java.net.URL;
    import java.util.Base64;
    
    public class Main {
        public static void main(String[] args) {
            Main example = new Main();
            example.createVoice();
        }
    
        public void createVoice() {
            // シンガポールリージョンと北京リージョンでは API キーが異なります。API キーを取得するには、https://www.alibabacloud.com/help/zh/model-studio/get-api-key をご参照ください。
            // 環境変数を設定していない場合は、次の行を置き換えてください: String apiKey = "sk-xxx"
            String apiKey = System.getenv("DASHSCOPE_API_KEY");
    
            // JSON リクエストボディ文字列を作成します
            String jsonBody = "{\n" +
                    "    \"model\": \"qwen-voice-design\",\n" +
                    "    \"input\": {\n" +
                    "        \"action\": \"create\",\n" +
                    "        \"target_model\": \"qwen3-tts-vd-realtime-2025-12-16\",\n" +
                    "        \"voice_prompt\": \"落ち着いた中年の男性アナウンサーで、深く豊かで魅力的な声、安定した話し方、明瞭な発音で、ニュース放送やドキュメンタリーのナレーションに適しています。\",\n" +
                    "        \"preview_text\": \"リスナーの皆様、こんにちは。イブニングニュースへようこそ。\",\n" +
                    "        \"preferred_name\": \"announcer\",\n" +
                    "        \"language\": \"en\"\n" +
                    "    },\n" +
                    "    \"parameters\": {\n" +
                    "        \"sample_rate\": 24000,\n" +
                    "        \"response_format\": \"wav\"\n" +
                    "    }\n" +
                    "}";
    
            HttpURLConnection connection = null;
            try {
                // シンガポールリージョンの URL。北京リージョンの場合は、https://dashscope.aliyuncs.com/api/v1/services/audio/tts/customization を使用してください。
                URL url = new URL("https://dashscope-intl.aliyuncs.com/api/v1/services/audio/tts/customization");
                connection = (HttpURLConnection) url.openConnection();
    
                // リクエストメソッドとヘッダーを設定します
                connection.setRequestMethod("POST");
                connection.setRequestProperty("Authorization", "Bearer " + apiKey);
                connection.setRequestProperty("Content-Type", "application/json");
                connection.setDoOutput(true);
                connection.setDoInput(true);
    
                // リクエストボディを送信します
                try (OutputStream os = connection.getOutputStream()) {
                    byte[] input = jsonBody.getBytes("UTF-8");
                    os.write(input, 0, input.length);
                    os.flush();
                }
    
                // 応答を取得します
                int responseCode = connection.getResponseCode();
                if (responseCode == HttpURLConnection.HTTP_OK) {
                    // 応答内容を読み取ります
                    StringBuilder response = new StringBuilder();
                    try (BufferedReader br = new BufferedReader(
                            new InputStreamReader(connection.getInputStream(), "UTF-8"))) {
                        String responseLine;
                        while ((responseLine = br.readLine()) != null) {
                            response.append(responseLine.trim());
                        }
                    }
    
                    // JSON 応答を解析します
                    JsonObject jsonResponse = JsonParser.parseString(response.toString()).getAsJsonObject();
                    JsonObject outputObj = jsonResponse.getAsJsonObject("output");
                    JsonObject previewAudioObj = outputObj.getAsJsonObject("preview_audio");
    
                    // 音声名を取得します
                    String voiceName = outputObj.get("voice").getAsString();
                    System.out.println("音声名: " + voiceName);
    
                    // Base64 エンコードされたオーディオデータを取得します
                    String base64Audio = previewAudioObj.get("data").getAsString();
    
                    // Base64 オーディオデータをデコードします
                    byte[] audioBytes = Base64.getDecoder().decode(base64Audio);
    
                    // オーディオをローカルファイルに保存します
                    String filename = voiceName + "_preview.wav";
                    saveAudioToFile(audioBytes, filename);
    
                    System.out.println("オーディオがローカルファイルに保存されました: " + filename);
    
                } else {
                    // エラー応答を読み取ります
                    StringBuilder errorResponse = new StringBuilder();
                    try (BufferedReader br = new BufferedReader(
                            new InputStreamReader(connection.getErrorStream(), "UTF-8"))) {
                        String responseLine;
                        while ((responseLine = br.readLine()) != null) {
                            errorResponse.append(responseLine.trim());
                        }
                    }
    
                    System.out.println("リクエストに失敗しました。ステータスコード: " + responseCode);
                    System.out.println("エラー応答: " + errorResponse.toString());
                }
    
            } catch (Exception e) {
                System.err.println("リクエストエラー: " + e.getMessage());
                e.printStackTrace();
            } finally {
                if (connection != null) {
                    connection.disconnect();
                }
            }
        }
    
        private void saveAudioToFile(byte[] audioBytes, String filename) {
            try {
                File file = new File(filename);
                try (FileOutputStream fos = new FileOutputStream(file)) {
                    fos.write(audioBytes);
                }
                System.out.println("オーディオが保存されました: " + file.getAbsolutePath());
            } catch (IOException e) {
                System.err.println("オーディオファイルの保存エラー: " + e.getMessage());
                e.printStackTrace();
            }
        }
    }
  2. 前のステップで生成されたカスタム音声を使用して音声合成を行います。

    この例は、システム音声を使用した音声合成のための DashScope SDK の「サーバーコミットモード」に基づいています。voice パラメーターを、音声デザインによって生成されたカスタム音声に置き換えます。

    重要な原則:音声デザイン中に使用されたモデル (target_model) は、後続の音声合成に使用されるモデル (model) と同じでなければなりません。そうでない場合、合成は失敗します。

    Python

    # coding=utf-8
    # pyaudio のインストール手順:
    # APPLE Mac OS X
    #   brew install portaudio
    #   pip install pyaudio
    # Debian/Ubuntu
    #   sudo apt-get install python-pyaudio python3-pyaudio
    #   または
    #   pip install pyaudio
    # CentOS
    #   sudo yum install -y portaudio portaudio-devel && pip install pyaudio
    # Microsoft Windows
    #   python -m pip install pyaudio
    
    import pyaudio
    import os
    import base64
    import threading
    import time
    import dashscope  # DashScope Python SDK バージョン 1.23.9 以降が必要です
    from dashscope.audio.qwen_tts_realtime import QwenTtsRealtime, QwenTtsRealtimeCallback, AudioFormat
    
    # ======= 定数構成 =======
    TEXT_TO_SYNTHESIZE = [
        'そうでしょう?こういうスーパーが本当に好きなんです、',
        '特に旧正月の時期は。',
        'スーパーに行くと',
        'なんだか',
        'すごく、すごく幸せな気分になります!',
        'たくさん買いたくなっちゃいます!'
    ]
    
    def init_dashscope_api_key():
        """
        DashScope SDK API キーを初期化します
        """
        # シンガポールリージョンと北京リージョンでは API キーが異なります。API キーを取得するには、https://www.alibabacloud.com/help/zh/model-studio/get-api-key をご参照ください。
        # 環境変数を設定していない場合は、次の行を置き換えてください: dashscope.api_key = "sk-xxx"
        dashscope.api_key = os.getenv("DASHSCOPE_API_KEY")
    
    # ======= コールバッククラス =======
    class MyCallback(QwenTtsRealtimeCallback):
        """
        カスタム TTS ストリーミングコールバック
        """
        def __init__(self):
            self.complete_event = threading.Event()
            self._player = pyaudio.PyAudio()
            self._stream = self._player.open(
                format=pyaudio.paInt16, channels=1, rate=24000, output=True
            )
    
        def on_open(self) -> None:
            print('[TTS] 接続が確立されました')
    
        def on_close(self, close_status_code, close_msg) -> None:
            self._stream.stop_stream()
            self._stream.close()
            self._player.terminate()
            print(f'[TTS] 接続が閉じられました、code={close_status_code}, msg={close_msg}')
    
        def on_event(self, response: dict) -> None:
            try:
                event_type = response.get('type', '')
                if event_type == 'session.created':
                    print(f'[TTS] セッションが開始されました: {response["session"]["id"]}')
                elif event_type == 'response.audio.delta':
                    audio_data = base64.b64decode(response['delta'])
                    self._stream.write(audio_data)
                elif event_type == 'response.done':
                    print(f'[TTS] 応答が完了しました、応答 ID: {qwen_tts_realtime.get_last_response_id()}')
                elif event_type == 'session.finished':
                    print('[TTS] セッションが終了しました')
                    self.complete_event.set()
            except Exception as e:
                print(f'[エラー] コールバックイベントの処理中に例外が発生しました: {e}')
    
        def wait_for_finished(self):
            self.complete_event.wait()
    
    # ======= メイン実行ロジック =======
    if __name__ == '__main__':
        init_dashscope_api_key()
        print('[システム] Qwen TTS Realtime を初期化中 ...')
    
        callback = MyCallback()
        qwen_tts_realtime = QwenTtsRealtime(
            # 音声デザインと音声合成には同じモデルを使用する必要があります
            model="qwen3-tts-vd-realtime-2025-12-16",
            callback=callback,
            # シンガポールリージョンの URL。北京リージョンの場合は、wss://dashscope.aliyuncs.com/api-ws/v1/realtime を使用してください。
            url='wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime'
        )
        qwen_tts_realtime.connect()
        
        qwen_tts_realtime.update_session(
            voice="myvoice", # voice パラメーターを、音声デザインによって生成されたカスタム音声に置き換えます
            response_format=AudioFormat.PCM_24000HZ_MONO_16BIT,
            mode='server_commit'
        )
    
        for text_chunk in TEXT_TO_SYNTHESIZE:
            print(f'[テキストを送信中]: {text_chunk}')
            qwen_tts_realtime.append_text(text_chunk)
            time.sleep(0.1)
    
        qwen_tts_realtime.finish()
        callback.wait_for_finished()
    
        print(f'[メトリック] session_id={qwen_tts_realtime.get_session_id()}, '
              f'first_audio_delay={qwen_tts_realtime.get_first_audio_delay()}s')

    Java

    import com.alibaba.dashscope.audio.qwen_tts_realtime.*;
    import com.alibaba.dashscope.exception.NoApiKeyException;
    import com.google.gson.JsonObject;
    
    import javax.sound.sampled.*;
    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 {
        // ===== 定数定義 =====
        private static String[] textToSynthesize = {
                "そうでしょう?こういうスーパーが本当に好きなんです、",
                "特に旧正月の時期は。",
                "スーパーに行くと",
                "なんだか",
                "すごく、すごく幸せな気分になります!",
                "たくさん買いたくなっちゃいます!"
        };
    
        // リアルタイムオーディオプレーヤークラス
        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<>();
    
            // コンストラクターはオーディオフォーマットとオーディオラインを初期化します
            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);
                            } 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();
                if (line != null && line.isRunning()) {
                    line.drain();
                    line.close();
                }
            }
        }
    
        public static void main(String[] args) throws Exception {
            QwenTtsRealtimeParam param = QwenTtsRealtimeParam.builder()
                    // 音声デザインと音声合成には同じモデルを使用する必要があります
                    .model("qwen3-tts-vd-realtime-2025-12-16")
                    // シンガポールリージョンの URL。北京リージョンの場合は、wss://dashscope.aliyuncs.com/api-ws/v1/realtime を使用してください。
                    .url("wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime")
                    // シンガポールリージョンと北京リージョンでは API キーが異なります。API キーを取得するには、https://www.alibabacloud.com/help/zh/model-studio/get-api-key をご参照ください。
                    // 環境変数を設定していない場合は、次の行を置き換えてください: .apikey("sk-xxx")
                    .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":
                            // セッション作成を処理します
                            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("myvoice") // voice パラメーターを、音声デザインによって生成されたカスタム音声に置き換えます
                    .responseFormat(QwenTtsRealtimeAudioFormat.PCM_24000HZ_MONO_16BIT)
                    .mode("server_commit")
                    .build();
            qwenTtsRealtime.updateSession(config);
            for (String text:textToSynthesize) {
                qwenTtsRealtime.appendText(text);
                Thread.sleep(100);
            }
            qwenTtsRealtime.finish();
            completeLatch.get().await();
    
            // オーディオ再生が完了するのを待ってからプレーヤーをシャットダウンします
            audioPlayer.waitForComplete();
            audioPlayer.shutdown();
            System.exit(0);
        }
    }

その他のサンプルコードについては、GitHub をご参照ください。

インタラクションフロー

Server_commit モード

session.update イベントの session.mode"server_commit" に設定して、このモードを有効にします。サーバーはその後、テキストのセグメンテーションと合成のタイミングを自動的に管理します。

インタラクションフローは以下の通りです:

  1. クライアントが session.update イベントを送信すると、サーバーは session.created および session.updated イベントで応答します。

  2. クライアントは input_text_buffer.append イベントを使用して、サーバー側のバッファーにテキストを追加します。

  3. サーバーはテキストのセグメンテーションと合成のタイミングをインテリジェントに管理し、response.createdresponse.output_item.addedresponse.content_part.added、および response.audio.delta イベントを返します。

  4. サーバーは、応答が完了した後、response.audio.doneresponse.content_part.doneresponse.output_item.done、および response.done イベントを送信します。

  5. サーバーは session.finished イベントを送信してセッションを終了します。

ライフサイクル

クライアントイベント

サーバーイベント

セッションの初期化

session.update

セッション構成

session.created

セッションが作成されました

session.updated

セッション構成が更新されました

ユーザーテキスト入力

input_text_buffer.append

サーバーにテキストを追加します

input_text_buffer.commit

サーバーにキャッシュされたテキストを即座に合成します

session.finish

これ以上テキスト入力がないことをサーバーに通知します

input_text_buffer.committed

サーバーが送信されたテキストを受信しました

サーバーオーディオ出力

なし

response.created

サーバーが応答の生成を開始します

response.output_item.added

応答に新しい出力コンテンツが利用可能です

response.content_part.added

アシスタントメッセージに新しい出力コンテンツが追加されます

response.audio.delta

モデルから増分生成されたオーディオ

response.content_part.done

アシスタントメッセージのテキストまたはオーディオコンテンツのストリーミングが完了しました

response.output_item.done

アシスタントメッセージの出力アイテム全体のストリーミングが完了しました

response.audio.done

オーディオ生成が完了しました

response.done

応答が完了しました

Commit モード

session.update イベントの session.mode"commit" に設定して、このモードを有効にします。このモードでは、クライアントは応答を受信するためにテキストバッファーをサーバーに送信する必要があります。

インタラクションフローは以下の通りです:

  1. クライアントが session.update イベントを送信すると、サーバーは session.created および session.updated イベントで応答します。

  2. クライアントは input_text_buffer.append イベントを送信して、サーバー側のバッファーにテキストを追加します。

  3. クライアントは input_text_buffer.commit イベントを送信してバッファーをサーバーにコミットし、session.finish イベントを送信してテキスト入力が完了したことを示します。

  4. サーバーは response.created イベントを送信して応答生成を開始します。

  5. サーバーは response.output_item.addedresponse.content_part.added、および response.audio.delta イベントを送信します。

  6. サーバーが応答した後、response.audio.doneresponse.content_part.doneresponse.output_item.done、および response.done イベントを送信します。

  7. サーバーは session.finished イベントを送信し、セッションを終了します。

ライフサイクル

クライアントイベント

サーバーイベント

セッションの初期化

session.update

セッション構成

session.created

セッションが作成されました

session.updated

セッション構成が更新されました

ユーザーテキスト入力

input_text_buffer.append

バッファーにテキストを追加します

input_text_buffer.commit

バッファーをサーバーにコミットします

input_text_buffer.clear

バッファーをクリアします

input_text_buffer.committed

サーバーがコミットされたテキストを受信しました

サーバーオーディオ出力

なし

response.created

サーバーが応答の生成を開始します

response.output_item.added

応答に新しい出力コンテンツが利用可能です

response.content_part.added

アシスタントメッセージに新しい出力コンテンツが追加されます

response.audio.delta

モデルから増分生成されたオーディオ

response.content_part.done

アシスタントメッセージのテキストまたはオーディオコンテンツのストリーミングが完了しました

response.output_item.done

アシスタントメッセージの出力アイテム全体のストリーミングが完了しました

response.audio.done

オーディオ生成が完了しました

response.done

応答が完了しました

API リファレンス

リアルタイム音声合成 - Qwen API リファレンス

音声クローン - API リファレンス

音声デザイン - API リファレンス

機能比較

特徴

qwen3-tts-vd-realtime-2025-12-16

qwen3-tts-vc-realtime-2025-11-27

qwen3-tts-flash-realtime, qwen3-tts-flash-realtime-2025-11-27, qwen3-tts-flash-realtime-2025-09-18

qwen-tts-realtime, qwen-tts-realtime-latest, qwen-tts-realtime-2025-07-15

サポートされている言語

中国語、英語、スペイン語、ロシア語、イタリア語、フランス語、韓国語、日本語、ドイツ語、ポルトガル語

中国語 (北京語、北京、上海、四川、南京、陝西、閩南、天津、広東語、音声によって異なる)、英語、スペイン語、ロシア語、イタリア語、フランス語、韓国語、日本語、ドイツ語、ポルトガル語

中国語と英語

オーディオフォーマット

pcm、wav、mp3、opus

pcm

オーディオサンプリングレート

8 kHz、16 kHz、24 kHz、48 kHz

24 kHz

音声クローン

非対応

対応

非対応

音声デザイン

対応

非対応

SSML

非対応

LaTeX

非対応

音量調整

対応

非対応

速度調整

対応

非対応

ピッチ調整

対応

非対応

ビットレート調整

対応

非対応

タイムスタンプ

非対応

感情設定

非対応

ストリーミング入力

対応

ストリーミング出力

対応

レート制限

1分あたりのリクエスト数 (RPM): 180

qwen3-tts-flash-realtime, qwen3-tts-flash-realtime-2025-11-27 RPM: 180

qwen3-tts-flash-realtime-2025-09-18 RPM: 10

RPM: 10

1分あたりのトークン数 (TPM): 100,000

アクセス方法

Java/Python/ SDK、WebSocket API

料金

国際 (シンガポール): 10,000 文字あたり 0.143353 ドル

中国本土 (北京): 10,000 文字あたり 0.143353 ドル

国際 (シンガポール): 10,000 文字あたり 0.13 ドル

中国本土 (北京): 10,000 文字あたり 0.143353 ドル

中国本土 (北京):

  • 入力コスト: 1,000 トークンあたり 0.345 ドル

  • 出力コスト: 1,000 トークンあたり 1.721 ドル

サポートされている音声

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