Skip to main content

音声合成のベストプラクティス

テキスト送信、音声処理、接続と同時実行リソースの管理、性能評価、問題診断に関する音声合成のベストプラクティスを説明します。

テキスト送信とタスク制御

クローン音声またはデザイン音声を使用する場合、合成モデルは音声の作成時に指定したターゲットモデルと一致させます。音声の作成要件は、音声クローンまたは音声デザインをご参照ください。

テキスト分割と送信タイミング

Qwen-Audio-TTS と CosyVoice に WebSocket で接続する場合、task-started を受信してからテキストを順番に送信します。サーバーは自動的に文を分割し、完全な文はすぐに合成します。不完全な文は、文が完成するか finish-task を受信するまでバッファーに保持します。 送信間隔が長いと合成の総所要時間が増加します。前の音声の再生終了までに次のテキストが届かない場合、再生も途切れます。1 回あたりの文字数、累積文字数、送信間隔の制限は、Qwen-Audio-TTS クライアントイベントまたは CosyVoice クライアントイベントをご参照ください。
SSML は全文を一度に送信します。分割入力には対応していません。対応モデル、音声、インターフェイスは SSML と LaTeXをご参照ください。

タスクの完了とキャンセル

WebSocket で Qwen-Audio-TTS または CosyVoice のプロトコルを直接使用する場合、テキスト送信後、finish-task で入力の終了を通知し、task-finished まで音声を受信します。終了通知を省略すると音声が不完全になる場合があります。 現在の合成を中断するには、タスクのキャンセルを使用します。サーバーはタスクを終了して task-finished を返します。SDK とプロトコル直接接続の使用方法および適用条件は、タスクのキャンセルをご参照ください。

音声の受信と再生

音声の受信と保存

Qwen-Audio-TTS と CosyVoice の音声の返却形式は、接続方式によって異なります。
  • HTTP の非ストリーミング出力:output.audio.url から完全な音声ファイルを取得します。URL の有効期間は 24 時間です。期限内に保存し、期限切れの場合は再合成します。
  • HTTP SSE 出力:output.audio.data は Base64 エンコードされた音声片です。受信順にデコードし、同じファイルに追記します。レスポンスフィールドは Qwen-Audio-TTS HTTP API と CosyVoice HTTP API をご参照ください。
  • WebSocket の直接接続:binary チャネルで音声を受信し、テキストイベントで文などの情報を受信します。処理フローは Qwen-Audio-TTS WebSocket API と CosyVoice WebSocket API をご参照ください。

音声形式とプレーヤーの互換性

完全なファイルとして保存する場合は、拡張子をリクエストの音声形式と一致させ、プレーヤーが形式とサンプルレートに対応することを確認します。ストリーミング再生の問題は、まずストリームをファイルに保存して切り分けます。ファイルも再生できなければ形式とプレーヤーを確認します。ファイルを再生できる場合は、ffmpeg、pyaudio、AudioFormat、MediaSource など、再生実装のストリーミング対応を確認します。 ストリーミング WAV のヘッダーには最終的な音声長が反映されない場合があります。正確な再生時間が必要な場合は、format を pcm に設定し、音声をすべて受信した後に実際の長さを含む WAV ヘッダーを追加します。

コールバックと再生のスケジューリング

リアルタイム合成のコールバックは WebSocket スレッドで実行されます。ブロッキングする業務処理は音声の受信を遅らせます。音声を別のバッファーに書き込み、別スレッドで処理できます。再生が途切れる場合は、テキストの送信速度とネットワークの安定性も確認します。ネットワークの変動によって音声転送が中断または遅延する場合があります。

接続と同時実行リソースの管理

接続の再利用とタスクの分離

Qwen-Audio-TTS と CosyVoice を WebSocket プロトコルで直接呼び出す場合、1 つの接続で複数のタスクを順次実行できます。task-finished を受信した後、新しい task_id で run-task を送信し、そのモデル系列の手順でテキストを送信します。キャンセル後も同じ手順で接続を再利用します。 Qwen-Audio-TTS、CosyVoice ではタスク完了後 60 秒のアイドルで切断されます。長時間アイドル後の呼び出しは新規接続として評価します。 Python SDK の client.audio.tts.connect は、タスクの完了またはキャンセル時に、既定でセッションの基になる接続を閉じます。再利用するには、client.ws_connection() で共有接続を作成し、connection= でセッションに渡します。呼び出し元が WsConnection.close で共有接続を閉じます。

接続の認証情報と分離範囲

API キーは WebSocket ハンドシェイク時の Authorization ヘッダーでのみ認証に使用され、後続の run-task メッセージには含まれません。接続の再利用中にパラメーターやグローバル設定のキーを変えても認証 ID は変わらず、認証主体、クォータ、課金先が想定と異なる可能性があります。
  • Java SDK
  • Python SDK
Java では API キーごとに独立したプールを作成し、同じプールでは常に同じキーを使用します。Qwen-Audio-TTS/CosyVoice では借り出すたびに updateParamAndCallback を呼び出しますが、このメソッドは既存接続を再認証しません。

同時実行数とリソースの計画

オブジェクトプールは合成オブジェクトと WebSocket 接続を保持し、オブジェクトの再作成、TCP/TLS ハンドシェイク、プロトコルのアップグレードに伴うオーバーヘッドを削減します。対応するモデルシリーズと SDK は以下のとおりです。
  • Qwen-Audio-TTS、CosyVoice:Python と Java のオブジェクトプール。
同時実行タスク数とサーバー構成に合わせてリソースプールを設定します。モデルのレート制限は Alibaba Cloud アカウント単位で集計され、RAM ユーザー、ワークスペース、API キーが同じアカウントのクォータを共有します。各モデルの呼び出し頻度と並列数の制限は、レート制限をご参照ください。 プール容量はピーク並列数の 1.5~2 倍を目安に設定し、実際の負荷に合わせて調整します。 Python のプールは、すべてのオブジェクトが貸出中でも追加のオブジェクトを作成する場合があります。追加オブジェクトを使用する際は新しい接続が必要です。
  • サーバー設定の参考値
  • Java リソースプールのパラメーター
以下は、対象の音声合成サービスのみを実行する場合の Alibaba Cloud サーバーの参考構成です。サーバーごとの並列数は同時実行中の合成タスク数を指します。実際の負荷に応じて構成を調整します。表の接続プール設定は、DASHSCOPE_CONNECTION_POOL_SIZE、DASHSCOPE_MAXIMUM_ASYNC_REQUESTS、DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST の共通の参考値です。この参考構成では 3 項目とも 2000 に設定します。オブジェクトプールの容量は各サンプルの環境変数で設定します。
シリーズAlibaba Cloud サーバー並列タスク数オブジェクトプール容量接続プール設定
CosyVoice4 vCPU / 8 GiB1005002000
CosyVoice8 vCPU / 16 GiB1505002000
CosyVoice16 vCPU / 32 GiB2005002000

オブジェクトのライフサイクル

API キーとエンドポイントを設定してから、業務タスクで共有するオブジェクトプールを作成します。
  • Java SDK
  • Python SDK
  1. borrowObject でオブジェクトを借り出します。Qwen-Audio-TTS/CosyVoice では、毎回 updateParamAndCallback(param, callback) を呼び出し、タスクのパラメーターとコールバックを設定します。
  2. Qwen-Audio-TTS/CosyVoice の合成は call または streamingCall で実行します。
  3. 正常完了後は returnObject で返却します。未完了または失敗した場合は直接返却せず、invalidateObject で破棄し、ファクトリーで接続を閉じます。
  4. すべてのタスクの終了後、プールの close() を呼び出してリソースを解放します。
Java では maxIdle が maxTotal より小さい場合、返却時にアイドル容量を超えるオブジェクトが破棄されます。ファクトリーの destroyObject が WebSocket を閉じないと、オブジェクトの破棄後も接続が残る可能性があります。
クライアントエラーでタスクが中断されても、サーバーでは前のタスクが実行中と認識されている場合があります。この接続で新しい run-task を送ると、プロトコルの順序エラー(Invalid action('run-task')! Please follow the protocol!)になります。接続を閉じてオブジェクトを破棄し、後続タスクは新しい接続で実行します。 古い接続を閉じずに合成オブジェクトを頻繁に作成すると、接続数が増加し続け、サーバーリソースの消費も増えます。タスク完了後のオブジェクト返却と、不要な接続の切断を確認します。

オブジェクトプールのコード例

  • API キーを取得し、DASHSCOPE_API_KEY を設定します。呼び出すリージョンのキーを使用してください。
  • {WorkspaceId} を対象リージョンのワークスペース IDに置換し、WebSocket エンドポイントを設定します。
  • Java SDK
  • Python SDK
  • Qwen-Audio-TTS、CosyVoice:DashScope Java SDK 2.16.6 以降。
Java の例には Apache Commons Pool 2 も必要です。Maven または Gradle で依存関係を設定します。
Maven
<dependencies>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>dashscope-sdk-java</artifactId>
        <version>2.23.2</version>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
        <version>2.12.0</version>
    </dependency>
</dependencies>
例では qwen-audio-3.1-tts-flash と longanhuan_v3.1 で 3 件の合成を並列実行します。CosyVoice ではモデルと音声を cosyvoice-v3-flash と longanyang に変更できます。その他のモデルと音声要件はリアルタイム音声合成をご参照ください。
Java
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.utils.Constants;
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.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;

public class PooledTts {
    static class Factory extends BasePooledObjectFactory<SpeechSynthesizer> {
        public SpeechSynthesizer create() { return new SpeechSynthesizer(); }
        public PooledObject<SpeechSynthesizer> wrap(SpeechSynthesizer value) {
            return new DefaultPooledObject<>(value);
        }
        public void destroyObject(PooledObject<SpeechSynthesizer> value) {
            value.getObject().getDuplexApi().close(1000, "done");
        }
    }

    static void synthesize(GenericObjectPool<SpeechSynthesizer> pool, int index) throws Exception {
        SpeechSynthesizer synthesizer = pool.borrowObject();
        boolean successful = false;
        AtomicReference<Exception> error = new AtomicReference<>();
        try (FileOutputStream audio = new FileOutputStream("output_" + index + ".mp3")) {
            SpeechSynthesisParam param = SpeechSynthesisParam.builder()
                .apiKey(System.getenv("DASHSCOPE_API_KEY"))
                .model("qwen-audio-3.1-tts-flash")
                .voice("longanhuan_v3.1")
                .format(SpeechSynthesisAudioFormat.MP3_24000HZ_MONO_256KBPS)
                .build();
            synthesizer.updateParamAndCallback(param, new ResultCallback<SpeechSynthesisResult>() {
                public void onEvent(SpeechSynthesisResult result) {
                    ByteBuffer buffer = result.getAudioFrame();
                    if (buffer != null) {
                        byte[] bytes = new byte[buffer.remaining()];
                        buffer.get(bytes);
                        try { audio.write(bytes); }
                        catch (Exception e) { error.set(e); }
                    }
                }
                public void onComplete() {}
                public void onError(Exception e) { error.set(e); }
            });
            synthesizer.streamingCall("Hello, welcome to Alibaba Cloud Model Studio.");
            synthesizer.streamingComplete(60000);
            if (error.get() != null) { throw error.get(); }
            successful = true;
        } finally {
            if (successful) { pool.returnObject(synthesizer); }
            else { pool.invalidateObject(synthesizer); }
        }
    }

    public static void main(String[] args) throws Exception {
        Constants.baseWebsocketApiUrl = "wss://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference";
        GenericObjectPoolConfig<SpeechSynthesizer> config = new GenericObjectPoolConfig<>();
        int poolSize = Integer.parseInt(System.getenv().getOrDefault("COSYVOICE_OBJECTPOOL_SIZE", "500"));
        config.setMaxTotal(poolSize);
        config.setMaxIdle(poolSize);
        config.setMaxWait(Duration.ofSeconds(60));
        ExecutorService executor = Executors.newFixedThreadPool(3);
        try (GenericObjectPool<SpeechSynthesizer> pool = new GenericObjectPool<>(new Factory(), config)) {
            java.util.List<Future<?>> futures = new java.util.ArrayList<>();
            for (int i = 0; i < 3; i++) {
                final int index = i;
                futures.add(executor.submit(() -> {
                    try { synthesize(pool, index); }
                    catch (Exception e) { throw new RuntimeException(e); }
                }));
            }
            for (Future<?> future : futures) { future.get(); }
        } finally {
            executor.shutdownNow();
        }
    }
}

性能評価と問題診断

Python のプールは初期化時に接続します。Java の GenericObjectPool は必要時にオブジェクトを作成し、最初の合成で接続を確立します。Java の接続をウォームアップするには、実際に合成リクエストを実行します。起動時やネットワーク復旧後はウォームアップを行い、並列数を徐々に増やします。性能試験前には目標の並列数で一定時間(例:1~2 分間)ウォームアップを行い、接続が安定して再利用される状態で測定を開始します。 同時実行数を段階的に増やすことで、大量の WebSocket 接続の同時確立によるブロッキングを避けられます。
初回呼び出しには WebSocket 接続の確立に伴う処理が含まれます。接続の再利用時とは分けて、最初の音声を受信するまでの時間とタスク全体の時間を測定します。合成速度はリアルタイム係数(RTF、合成の総所要時間 / 音声の長さ)でも評価できます。SDK 側の最初の音声受信までの遅延には接続確立やネットワーク転送の時間も含まれ、モデルの推論時間だけを示すものではありません。Qwen-Audio-TTS/CosyVoice の Python オブジェクトでは、get_first_package_delay() で最初の音声受信までの遅延を、get_last_request_id() でログとの照合に使うリクエスト ID を取得できます。Java オブジェクトでは getLastRequestId() でリクエスト ID を取得します。
Java のオブジェクトプールは maxTotal に達すると、利用可能なオブジェクトを待機します。容量、返却、接続のクリーンアップを確認し、借り出し待機による遅延を防ぎます。
起動時、ネットワーク切断後、または大量のサーバーエラー後に再接続が集中すると、タスクが滞留する場合があります。接続の復旧後に滞留タスクが同時送信されると、呼び出しが急増してアカウントのレート制限に達する可能性があります。急増の直前にネットワーク障害や大量のサーバーエラーがなかったかを確認します。例:Requests rate limit exceeded, please try again later.。 起動時と復旧時の並列数を制御します。Java では maxTotal で同じプールから同時に借り出せるオブジェクト数を制限できます。アカウントのクォータを確認し、引き上げが必要な場合はレート制限の手順に従って申請します。 実際の負荷に合わせてサーバー構成を調整するか、台数を増やします。
ネットワーク帯域幅が上限に達していないか、実際の並列数が高すぎないかを確認し、負荷に応じて並列数やサーバー構成を調整します。