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

Alibaba Cloud Model Studio:CosyVoice 音声合成 WebSocket API

最終更新日:Mar 08, 2026

本トピックでは、WebSocket 接続を用いた CosyVoice 音声合成サービスへのアクセス方法について説明します。

DashScope SDK は Java および Python のみをサポートしています。その他のプログラミング言語で CosyVoice 音声合成アプリケーションを構築する場合は、WebSocket 接続を用いてサービスと通信してください。

ユーザーガイド:モデルの概要および選択に関する推奨事項については、「リアルタイム音声合成 — CosyVoice」をご参照ください。

WebSocket は、全二重通信をサポートするネットワークプロトコルです。クライアントとサーバーは単一のハンドシェイクにより持続的接続を確立し、双方が相互にデータを能動的にプッシュできます。これにより、リアルタイム性能および効率性において顕著な利点が得られます。

一般的なプログラミング言語向けには、以下のような多数の利用可能な WebSocket ライブラリおよびサンプルが提供されています。

  • Go:gorilla/websocket

  • PHP:Ratchet

  • Node.js:ws

開発を開始する前に、WebSocket の基本原理および技術的詳細について十分に理解してください。

重要

CosyVoice モデルは WebSocket 接続のみをサポートしており、HTTP REST API はサポートしていません。 HTTP リクエスト(例:POST)を用いてサービスを呼び出した場合、サービスから InvalidParameter エラーまたは URL エラーが返されます。

前提条件

API キーを取得済みです。

モデルおよび課金

リアルタイム音声合成 — CosyVoice」をご参照ください。

音声合成テキストの文字数制限およびフォーマット規則

テキスト長の制限

continue-task 命令 には、最大 20,000 文字まで含めることができます。すべての continue-task 命令 の呼び出しにおいて、送信される文字数の合計は 200,000 を超えてはなりません。

文字数カウント規則

  • 中国語文字(簡体字・繁体字、日本語漢字、韓国語漢字を含む)は 2 文字としてカウントされます。句読点、英数字、日本語/韓国語の仮名/ハングルなどその他のすべての文字は 1 文字としてカウントされます。

  • SSML タグはテキスト長の計算対象には含まれません。

  • 例:

    • "你好" → 2(中国語文字)+ 2(中国語文字)= 4 文字

    • "中A文123" → 2(中国語文字)+ 1(A)+ 2(中国語文字)+ 1(1)+ 1(2)+ 1(3)= 8 文字

    • "中文。" → 2(中国語文字)+ 2(中国語文字)+ 1(.)= 5 文字

    • "中 文。" → 2(中国語文字)+ 1(半角スペース)+ 2(中国語文字)+ 1(.)= 6 文字

    • "<speak>你好</speak>" → 2(中国語文字)+ 2(中国語文字)= 4 文字

エンコーディング形式

UTF-8 エンコーディングを使用してください。

数式のサポート

数式のパース機能は、cosyvoice-v3.5-flash、cosyvoice-v3.5-plus、cosyvoice-v3-flash、cosyvoice-v3-plus、cosyvoice-v2 のみで利用可能です。小学校および中学校レベルの基本的な演算、代数、幾何学などの一般的な数式をサポートしています。

説明

この機能は中国語のみをサポートします。

LaTeX 数式を音声に変換(中国語のみ)」をご参照ください。

SSML マークアップ言語のサポート

SSML を使用するには、以下のすべての条件を満たす必要があります。

  1. モデルのサポート: SSML をサポートするモデルは、cosyvoice-v3.5-flash、cosyvoice-v3.5-plus、cosyvoice-v3-flash、cosyvoice-v3-plus、cosyvoice-v2 のみです。

  2. 音声のサポート: SSML 対応音声を使用する必要があります。サポートされる音声は以下のとおりです。

    • すべてのクローン音声(音声クローン API を用いて作成されたカスタム音声)。

    • 音声一覧で SSML 対応とマークされたシステム音声。

    説明

    SSML をサポートしないシステム音声(一部の基本音声など)を使用した場合、enable_ssml パラメーターを有効化すると「SSML text is not supported at the moment!(現在、SSML テキストはサポートされていません!)」というエラーが返されます。

  3. パラメーターの設定: run-task 命令 内で、enable_ssml パラメーターを true に設定します。

すべての条件を満たした後、continue-task 命令 を用いて SSML 形式のテキストを送信します。完全な例については、「クイックスタート」をご参照ください。

インタラクションフロー

image

クライアントからサーバーへ送信されるメッセージは 命令 と呼ばれます。サーバーはクライアントに対して、JSON 形式の イベント およびバイナリ音声ストリームの 2 種類のメッセージを返します。

時系列順に、クライアントとサーバー間のインタラクションフローは以下のとおりです。

  1. 接続の確立:クライアントがサーバーとの WebSocket 接続を確立します。

  2. タスクの開始:クライアントが run-task 命令 を送信してタスクを開始します。

  3. 確認の待機:クライアントがサーバーから task-started イベント を受信し、タスクが正常に開始されたことを確認します。その後、以降のステップを実行できます。

  4. 合成対象テキストの送信:

    クライアントは、合成対象テキストを含む 1 つ以上の continue-task 命令 を順次送信します。サーバーは完全な文を受信すると、result-generated イベント および音声ストリームを返します。(テキスト長の制約が適用されます。「continue-task 命令」内の text フィールドの説明をご参照ください。)

    説明

    複数の continue-task 命令 を送信し、テキストフラグメントを順序立てて提出できます。サーバーは自動的にそれらを文に分割します。

    • 完全な文は即座に合成され、クライアントはすぐに音声を受信します。

    • 不完全な文はキャッシュされ、完全になるまで合成されません。不完全な文については音声は返されません。

    finish-task 命令 を送信すると、サーバーはキャッシュされたすべてのコンテンツを強制的に合成します。

  5. 音声の受信: binary チャンネルを介して音声ストリームを受信します。

  6. タスク終了の通知:

    すべてのテキストを送信した後、クライアントは finish-task 命令 を送信し、サーバーにタスク終了を通知します。その後もサーバーからの音声を継続して受信してください。このステップをスキップしないでください。そうしないと、音声や音声ストリームの終端を欠落させる可能性があります。

  7. タスクの終了:

    クライアントがサーバーから task-finished イベント を受信すると、タスクが終了したことを示します。

  8. 接続の閉じる:クライアントが WebSocket 接続を閉じます。

リソース使用効率を向上させるため、各タスクごとに新しい接続を開くのではなく、単一の WebSocket 接続を複数のタスクで再利用してください。「接続オーバーヘッドおよび再利用」をご参照ください。

重要

task_id は一貫して維持する必要があります。1 つの合成タスクにおいて、run-task、すべての continue-task、および finish-task 命令 は同じ task_id を使用しなければなりません。

エラーによる影響: 異なる task_id を使用すると、以下の問題が発生します。

  • サーバーがリクエストを関連付けられず、音声ストリームの配信が乱れる。

  • テキストコンテンツが誤ったタスクに割り当てられ、音声の整合性が損なわれる。

  • タスクのステータスが異常になり、task-finished イベントが欠落する可能性がある。

  • 課金が失敗し、使用量統計が不正確になる。

正しい方法:

  • run-task を送信する際に、一意の task_id(UUID を使用するなど)を生成します。

  • その task_id を変数に格納します。

  • その後のすべての continue-task および finish-task 命令で、同一の task_id を使用します。

  • タスクが終了した後(task-finished を受信した後)、新しいタスクでは新しい task_id を生成します。

クライアント実装に関する注意事項

WebSocket クライアント(特に Flutter、Web、モバイルプラットフォーム上)を実装する際は、サーバーとクライアントの責任範囲を明確に定義し、完全かつ安定した音声合成タスクを保証してください。

サーバーおよびクライアントの責任

サーバーの責任

サーバーは、順序通りに完全な音声ストリームを返すことを保証します。音声データの順序や完全性を心配する必要はありません。サーバーは、テキストの順序に従ってすべての音声チャンクを生成・プッシュします。

クライアントの責任

クライアントは、以下の重要なタスクを処理する必要があります。

  1. すべての音声チャンクの読み取りおよび連結

    サーバーは音声を複数のバイナリフレームとして送信します。クライアントはすべてのチャンクを受信し、最終的な音声ファイルを形成するために連結する必要があります。サンプルコードを以下に示します。

    # Python の例:音声チャンクの連結
    with open("output.mp3", "ab") as f:  # 追加モード
        f.write(audio_chunk)  # audio_chunk は各受信したバイナリ音声チャンク
    // JavaScript の例:音声チャンクの連結
    const audioChunks = [];
    ws.onmessage = (event) => {
      if (event.data instanceof Blob) {
        audioChunks.push(event.data);  // すべての音声チャンクを収集
      }
    };
    // タスク完了後に音声をマージ
    const audioBlob = new Blob(audioChunks, { type: 'audio/mp3' });
  2. WebSocket のライフサイクル全体の管理

    音声合成タスクの全体を通して——run-task 命令 の送信から task-finished イベント の受信まで——WebSocket を早期に切断しないでください。よくある誤りを以下に示します。

    • すべての音声チャンクが到着する前に接続を閉じ、音声が不完全になる。

    • finish-task 命令 を送信し忘れ、キャッシュされたテキストが未処理のままになる。

    • ページ遷移やアプリがバックグラウンドに移行した際に、WebSocket のキープアライブを適切に処理できない。

    重要

    モバイルアプリ(Flutter、iOS、Android)では、バックグラウンドへの移行時にネットワーク接続を慎重に管理する必要があります。バックグラウンドタスクまたはフォアグラウンドサービス内で WebSocket を維持するか、フォアグラウンド復帰時にタスク状態を確認して再接続してください。

  3. ASR→LLM→TTS ワークフローにおけるテキストの整合性

    ASR→LLM→TTS ワークフローでは、TTS に渡すテキストが途中で途切れないよう、完全な状態であることを保証してください。具体的には以下のようにします。

    • LLM が完全な文または段落を生成するのを待ってから、continue-task 命令 を送信します。文字単位でのストリーミング送信は避けてください。

    • ストリーミング合成が必要な場合(再生中に生成する場合)、ピリオドや疑問符などの自然な文の区切りでテキストを送信します。

    • LLM の出力が完了したら、必ず finish-task 命令 を送信し、末尾のコンテンツを欠落させないようにしてください。

プラットフォーム固有のヒント

  • Flutter: web_socket_channel パッケージを使用する場合、dispose メソッドで接続を正しく閉じることで、メモリリークを防止します。また、アプリのライフサイクルイベント(例:AppLifecycleState.paused)を処理して、バックグラウンドへの移行に対応します。

  • Web(ブラウザ): 一部のブラウザでは WebSocket 接続数が制限されています。複数のタスクで 1 つの接続を再利用してください。beforeunload イベントを使用して、ページ終了前に接続を閉じ、古くなった接続を回避します。

  • モバイル(iOS/Android ネイティブ): OS はアプリがバックグラウンドに移行した際にネットワーク接続を一時停止または終了することがあります。WebSocket をアクティブに保つためにバックグラウンドタスクまたはフォアグラウンドサービスを使用するか、フォアグラウンド復帰時にタスクを再初期化してください。

URL

WebSocket URL は固定です。

国際

国際デプロイモード」では、アクセスポイントおよびデータストレージの両方が シンガポールリージョン に配置されます。モデル推論のコンピューティングリソースは中国本土を除き、グローバルに動的にスケジュールされます。

WebSocket URL:wss://dashscope-intl.aliyuncs.com/api-ws/v1/inference

中国本土

中国本土デプロイモード」では、アクセスポイントおよびデータストレージの両方が 北京リージョン に配置されます。モデル推論のコンピューティングリソースは中国本土内に限定されます。

WebSocket URL:wss://dashscope.aliyuncs.com/api-ws/v1/inference

重要

一般的な URL 構成エラー:

  • エラー: http:// や https:// で始まる URL を使用 → 正しい方法: wss:// プロトコルを必ず使用してください。

  • エラー: Authorization パラメーターを URL クエリ文字列に含める(例:?Authorization=bearer <your_api_key>) → 正しい方法: Authorization パラメーターは HTTP ハンドシェイクのヘッダーに設定してください。「ヘッダー」をご参照ください。

  • エラー: URL の末尾にモデル名やその他のパスパラメーターを追加 → 正しい方法: URL は固定です。run-task 命令 内の payload.model パラメーターでモデルを指定してください。

ヘッダー

リクエストに以下のヘッダーを追加してください。

パラメーター

必須

説明

Authorization

string

はい

認証トークンは Bearer <your_api_key> フォーマットです。<your_api_key> を実際の API キーに置き換えてください。

user-agent

string

いいえ

サーバーがソースをトレースできるようにするクライアント識別子。

X-DashScope-WorkSpace

string

いいえ

Alibaba Cloud Model Studio の ワークスペース ID

X-DashScope-DataInspection

string

いいえ

データコンプライアンス検査を有効にするかどうか。デフォルトは未設定または enable です。必要でない限り有効化しないでください。

重要

認証タイミングおよび一般的なエラー

認証検証は WebSocket ハンドシェイク時に実行され、run-task 命令 を送信したときではありません。Authorization ヘッダーが存在しない、または API キーが無効な場合、サーバーはハンドシェイクを拒否し、HTTP 401 または 403 エラーを返します。クライアントライブラリでは、通常 WebSocketBadStatus 例外として報告されます。

認証失敗のトラブルシューティング

WebSocket 接続が失敗した場合、以下の手順に従ってください。

  1. API キーのフォーマットを確認: Authorization ヘッダーが bearer <your_api_key> のフォーマットであり、`bearer` と API キーの間に半角スペースがあることを確認してください。

  2. API キーの有効性を確認: Model Studio コンソールで、API キーが削除または無効化されておらず、CosyVoice モデルを呼び出す権限を持っていることを確認してください。

  3. ヘッダーの設定を確認: Authorization ヘッダーが WebSocket ハンドシェイク時に正しく設定されていることを確認してください。ヘッダーの設定方法はプログラミング言語によって異なります。

    • Python(websockets ライブラリ):extra_headers={"Authorization": f"bearer {api_key}"}

    • JavaScript:標準の WebSocket API はカスタムヘッダーをサポートしていません。サーバーサイドプロキシまたは ws などの別のライブラリを使用してください。

    • Go(gorilla/websocket):header.Add("Authorization", fmt.Sprintf("bearer %s", apiKey))

  4. ネットワーク接続をテスト: curl や Postman を使用して、API キーが他の HTTP サポートの DashScope API で正常に動作することをテストできます。

ブラウザ環境における WebSocket の使用

Vue3 や React などのブラウザ環境では、ネイティブのブラウザ WebSocket API にはセキュリティ制限があり、カスタムヘッダーの使用が禁止されています。ネイティブの new WebSocket(url) API は ハンドシェイク時のカスタムリクエストヘッダー(例:Authorization)を許可しません。これはブラウザのセキュリティ制限です。そのため、フロントエンドコードで API キーを直接使用して認証することはできません。

解決策: バックエンドプロキシを使用

  1. バックエンドサービス(Node.js、Java、Python など)から CosyVoice サービスへの WebSocket 接続を確立します。バックエンドサービスでは、Authorization ヘッダーを正しく設定できます。

  2. フロントエンドをバックエンドサービスに WebSocket で接続します。バックエンドサービスは、CosyVoice へのメッセージ転送を行うプロキシとして機能します。

  3. メリット:このアプローチにより、API キーをバックエンドで安全に保持できます。また、認証、ログ記録、レート制限などのビジネスロジックをバックエンドで追加できます。

重要

API キーをフロントエンドコードにハードコードしたり、ブラウザから直接送信したりしないでください。API キーを公開すると、アカウントの乗っ取り、高額な課金、データ侵害などのリスクがあります。

サンプルコード:

その他のプログラミング言語については、これらの例のロジックを流用するか、AI ツールを用いて変換してください。

  • フロントエンド(ネイティブ Web)+バックエンド(Node.js Express):cosyvoiceNodeJs_en.zip

  • フロントエンド(ネイティブ Web)+バックエンド(Python Flask):cosyvoiceFlask_en.zip

命令(クライアント → サーバー)

命令は、クライアントからサーバーへ送信される JSON 形式のメッセージであり、Text Frame 形式を用いてタスクの開始、停止、境界を制御します。

命令は厳密な順序で送信してください。そうでないとタスクが失敗する可能性があります。

  1. run-task 命令 を送信

    • 音声合成タスクを開始します。

    • 返される task_id は、その後の continue-task および finish-task 命令 で一貫して使用する必要があります。

  2. continue-task 命令 を送信

    • 合成対象テキストを送信します。

    • task-started イベント をサーバーから受信した後のみ送信してください。

  3. finish-task 命令 を送信

    • 音声合成タスクを終了します。

    • すべての continue-task 命令 を送信した後に送信してください。

1. run-task 命令:タスクの開始

この命令は音声合成タスクを開始します。音声、サンプルレート、その他のリクエストパラメーターをここで設定できます。

重要
  • 送信タイミング: WebSocket 接続が確立された後。

  • 合成対象テキストを送信しないでください: run-task 命令にテキストを含めて送信すると、トラブルシューティングが困難になります。これを避けてください。代わりに、continue-task 命令 を用いてテキストを送信してください。

  • input フィールドは必須です: payload には input フィールド({})が含まれている必要があります。省略すると「task can not be null」というエラーメッセージが返されます。

例:

{
    "header": {
        "action": "run-task",
        "task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx", // ランダムな UUID
        "streaming": "duplex"
    },
    "payload": {
        "task_group": "audio",
        "task": "tts",
        "function": "SpeechSynthesizer",
        "model": "cosyvoice-v3-flash",
        "parameters": {
            "text_type": "PlainText",
            "voice": "longanyang",            // 音声
            "format": "mp3",		        // 音声フォーマット
            "sample_rate": 22050,	        // サンプルレート
            "volume": 50,			// ボリューム
            "rate": 1,				// 話速
            "pitch": 1				// ピッチ
        },
        "input": {// input は必須であり、省略できません(空のオブジェクト {} を使用)
        }
    }
}

header パラメーター参照:

パラメーター

必須

説明

header.action

string

はい

命令タイプ。

この命令では、常に "run-task" です。

header.task_id

string

はい

このリクエストのタスク ID。

32 文字の普遍的ユニーク識別子(UUID)。32 文字のランダムな英数字で構成され、ハイフンを含む場合(例:"2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx")と含まない場合(例:"2bf83b9abaeb4fda8d9axxxxxxxxxxxx")があります。ほとんどの言語には組み込みの UUID 生成 API が提供されています。たとえば Python では:

import uuid

def generateTaskId(self):
    # ランダムな UUID を生成
    return uuid.uuid4().hex

後の continue-task および finish-task 命令 では、run-task 命令で使用したのと同じ task_id を使用してください。

header.streaming

string

はい

固定文字列:"duplex"

payload パラメーター参照:

パラメーター

必須

説明

payload.task_group

string

はい

固定文字列:"audio"。

payload.task

string

はい

固定文字列:"tts"。

payload.function

string

はい

固定文字列:"SpeechSynthesizer"。

payload.model

string

はい

音声合成の モデル

異なるモデルバージョンでは、対応する音声バージョンが必要です。

  • cosyvoice-v3.5-flash/cosyvoice-v3.5-plus:システム音声は利用できません。音声デザインまたは音声クローンで作成されたカスタム音声のみがサポートされます。

  • cosyvoice-v3-flash/cosyvoice-v3-plus:longanyang などの音声を使用します。

  • cosyvoice-v2:longxiaochun_v2 などの音声を使用します。

  • 音声の一覧については、「音声一覧」をご参照ください。

payload.input

object

はい

run-task 命令では、input フィールドは必須(省略不可)ですが、ここでは合成対象テキストを送信しないでください(空のオブジェクト {} を使用)。トラブルシューティングおよびストリーミング合成を容易にするため、合成対象テキストは後の continue-task 命令 で送信してください。

input のフォーマットは以下のとおりです。

"input": {}
重要

よくあるエラー: input フィールドを省略したり、mode や content などの予期しないフィールドを追加したりすると、サーバーが「InvalidParameter: task can not be null」エラーでリクエストを拒否したり、接続を閉じたり(WebSocket コード 1007)します。

payload.parameters

text_type

string

はい

固定文字列:"PlainText"。

voice

string

はい

音声合成に使用する音声。

システム音声およびクローン音声がサポートされます。

  • システム音声: 詳細については、「音声一覧」をご参照ください。

  • クローン音声: 音声クローン 機能を用いてカスタマイズされた音声。クローン音声を使用する場合、音声クローンと音声合成の両方で同一のアカウントを使用していることを確認してください。

    クローン音声を使用する場合、このリクエストの model パラメーターの値は、音声を作成する際に使用したモデルバージョン(target_model パラメーター)と一致している必要があります。

  • デザイン音声: 音声デザイン 機能を用いてカスタマイズされた音声。デザイン音声を使用する場合、音声デザインと音声合成の両方で同一のアカウントを使用していることを確認してください。

    デザイン音声を使用する場合、このリクエストの model パラメーターの値は、音声を作成する際に使用したモデルバージョン(target_model パラメーター)と一致している必要があります。

format

string

いいえ

音声コーディングフォーマット。

pcm、wav、mp3(デフォルト)、opus をサポートします。

format が opus の場合、bit_rate パラメーターでビットレートを調整します。

sample_rate

integer

いいえ

音声のサンプリングレート(Hz 単位)。

デフォルト:22050。

有効な値:8000、16000、22050、24000、44100、48000。

説明

デフォルトのサンプルレートは、選択した音声にとって最適なレートを表します。出力はこのレートでデフォルトで行われますが、ダウンサンプリングおよびアップサンプリングもサポートされます。

volume

integer

いいえ

ボリューム。

デフォルト値:50。

有効な値:[0, 100]。値 50 はデフォルトのボリュームを表します。ボリュームはこの値に比例して変化します。値 0 は無音、値 100 は最大ボリュームです。

rate

float

いいえ

話速。

デフォルト値:1.0。

有効な値:[0.5, 2.0]。値 1.0 は標準の話速です。1.0 より小さい値は話速を遅くし、1.0 より大きい値は話速を速くします。

pitch

float

いいえ

ピッチ。この値はピッチ調整の乗数として機能しますが、知覚されるピッチ変化との関係は厳密に線形でも対数的でもありません。適切な値を見つけるにはテストを推奨します。

デフォルト値:1.0。

有効な値:[0.5, 2.0]。値 1.0 は音声の自然なピッチです。1.0 より大きい値はピッチを上げ、1.0 より小さい値はピッチを下げます。

enable_ssml

boolean

いいえ

SSML を有効化します。

true に設定すると、テキストを一度だけ送信できます(continue-task 命令 は 1 回のみ許可されます)。

bit_rate

int

いいえ

音声ビットレート(kbps)。音声フォーマットが Opus の場合、bit_rate パラメーターでビットレートを調整します。

デフォルト値:32。

有効な値:[6, 510]。

word_timestamp_enabled

ブール値

いいえ

単語レベルのタイムスタンプを有効にするかどうかを指定します。

デフォルト値:false

  • true

  • false

この機能は、cosyvoice-v3-flash、cosyvoice-v3-plus、および cosyvoice-v2 モデルのクローン音声、および音声リストでサポート対応とマークされたシステム音声でのみ利用可能です。

word_timestamp_enabled を有効にすると、タイムスタンプ情報が result-generated イベント に表示されます。例:

{
  "header": {
    "task_id": "3f39be22-efbd-4844-91d5-xxxxxxxxxxxx",
    "event": "result-generated",
    "attributes": {}
  },
  "payload": {
    "output": {
      "sentence": {
        "index": 0,
        "words": [
          {
            "text": "bed",
            "begin_index": 0,
            "end_index": 1,
            "begin_time": 280,
            "end_time": 640
          }
        ]
      },
      "type": "sentence-begin",
      "original_text": "Before my bed, moonlight shines bright,"
    }
  }
}

seed

int

いいえ

生成時に使用される乱数シード。異なるシードは異なる合成結果を生成します。モデル、テキスト、音声、その他のパラメーターが同一の場合、同一のシードを使用すると同一の出力を再現できます。

デフォルト値:0。

有効な値:[0, 65535]。

language_hints

array[string]

いいえ

音声合成の対象言語を指定し、合成効果を向上させます。

数字、略語、記号の発音や、あまり使われない言語の合成品質が期待通りでない場合に、このパラメーターを使用します。例:

  • 数字が期待どおりに読み上げられません。例えば、"hello, this is 110" は "hello, this is yāo yāo líng" ではなく "hello, this is one one zero" と読み上げられます。

  • @ 記号は「アット」ではなく、「あい て」と誤って発音されます。

  • あまり使われない言語の合成品質が低く、不自然に聞こえる場合。

有効な値:

  • zh:中国語

  • en:英語

  • fr:フランス語

  • de:ドイツ語

  • ja:日本語

  • ko:韓国語

  • ru:ロシア語

  • pt:ポルトガル語

  • th:タイ語

  • id:インドネシア語

  • vi:ベトナム語

注: このパラメーターは配列ですが、現在のバージョンでは最初の要素のみが処理されます。そのため、1 つの値のみを渡すことを推奨します。

重要

このパラメーターは音声合成の対象言語を指定します。これは音声クローンに使用するサンプル音声の言語とは独立しています。クローンタスクのソース言語を設定するには、「CosyVoice 音声クローン/デザイン API」をご参照ください。

instruction

string

いいえ

方言、感情、話し方のスタイルなどの合成効果を制御する命令を設定します。この機能は、cosyvoice-v3.5-flash、cosyvoice-v3.5-plus、cosyvoice-v3-flash モデルのクローン音声および、音声一覧 で Instruct をサポートとマークされたシステム音声でのみ利用可能です。

長さ制限: 100 文字。

中国語文字(簡体字・繁体字、日本語漢字、韓国語漢字を含む)は 2 文字としてカウントされます。句読点、英数字、日本語/韓国語の仮名/ハングルなどその他のすべての文字は 1 文字としてカウントされます。

使用要件(モデルによって異なります):

  • cosyvoice-v3.5-flash および cosyvoice-v3.5-plus:感情や話速などの合成効果を制御する任意の命令を入力できます。

    重要

    cosyvoice-v3.5-flash および cosyvoice-v3.5-plus にはシステム音声がありません。音声デザインまたは音声クローンで作成されたカスタム音声のみがサポートされます。

    命令の例:

    非常に興奮し、高いピッチで話すようにしてください。大きな成功に対する陶酔感と興奮を表現します。
    中程度のゆっくりした話速を保ち、洗練され知的なトーンで話してください。落ち着きと安心感を与えるようにします。
    悲しみと郷愁に満ちたトーンで話してください。少し鼻にかかった質感を伴い、心を打つ物語を語っているかのようにします。
    息漏れのあるトーンで、非常に小さな音量で話してください。親密で神秘的なささやきの感覚を創出します。
    非常に不耐烦で苛立ったトーンで話してください。話速を速め、文の間の休止を最小限に抑えます。
    優しく穏やかな年配者を模倣し、一定の話速で、思いやりと愛情に満ちたトーンで話してください。
    皮肉で軽蔑的なトーンで話してください。キーワードを強調し、文末でわずかに上がり調子のイントネーションを用います。
    極度に恐れ、震えた声で話してください。
    プロのニュースアナウンサーのように話してください。冷静で客観的、明瞭な発音で、中立的な感情を込めて話します。
    生き生きとした遊び心のあるトーンで話してください。明るい笑顔をイメージし、エネルギッシュで陽気な声を出します。
  • cosyvoice-v3-flash:以下の要件を満たす必要があります。

    • クローン音声:音声合成効果を制御するために、任意の自然言語を使用できます。

      命令の例:

      広東語で話してください。(サポートされる方言:広東語、東北地方、甘粛省、貴州省、河南省、湖北省、江西省、閩南語、寧夏回族自治区、山西省、陝西省、山東省、上海語、四川省、天津市、雲南省)
      できるだけ大きな声で一文を言ってください。
      できるだけゆっくりと一文を言ってください。
      できるだけ速く一文を言ってください。
      とても小さく一文を言ってください。
      もう少しゆっくり話してもらえますか?
      とても速く話してもらえますか?
      とてもゆっくり話してもらえますか?
      もう少し速く話してもらえますか?
      とても怒ったトーンで一文を言ってください。
      とても幸せなトーンで一文を言ってください。
      とても恐れたトーンで一文を言ってください。
      とても悲しいトーンで一文を言ってください。
      とても驚いたトーンで一文を言ってください。
      できるだけ力強いトーンで話してください。
      できるだけ怒ったトーンで話してください。
      親しみやすいトーンで話してください。
      冷たいトーンで話してください。
      威厳のあるトーンで話してください。
      自然なトーンを体験したいです。
      脅しの表現を見てみたいと思います。
      知恵の表現を見てみたいと思います。
      誘惑の表現を見てみたいと思います。
      生き生きとしたトーンで話してみたいと思います。
      情熱を持って話してみたいと思います。
      落ち着いたトーンで話してみたいと思います。
      自信を持って話してみたいと思います。
      興奮して話してもらえますか?
      傲慢な感情を示してもらえますか?
      洗練された感情を示してもらえますか?
      喜んで質問に答えてもらえますか?
      優しい感情のデモンストレーションをしてもらえますか?
      落ち着いたトーンで話してもらえますか?
      深みのあるトーンで答えてもらえますか?
      荒々しい態度で話してもらえますか?
      邪悪な声で答えを教えてください。
      しなやかな声で答えを教えてください。
      自然で親しいチャットスタイルで語ってください。
      ラジオドラマのポッドキャスターのトーンで話してください。
    • システム音声:命令は固定のフォーマットおよび内容でなければなりません。「音声一覧」をご参照ください。

enable_aigc_tag

boolean

いいえ

生成された音声に不可視の AIGC 識別子を追加するかどうかを指定します。true に設定すると、サポートされるフォーマット(WAV、MP3、Opus)の音声に不可視の識別子が埋め込まれます。

デフォルト値:false。

この機能は cosyvoice-v3-flash、cosyvoice-v3-plus、cosyvoice-v2 のみでサポートされます。

aigc_propagator

string

いいえ

不可視の AIGC 識別子の ContentPropagator フィールドを設定し、コンテンツ伝播元を識別します。このパラメーターは enable_aigc_tagtrue の場合にのみ有効です。

デフォルト値:Alibaba Cloud UID。

この機能は cosyvoice-v3-flash、cosyvoice-v3-plus、cosyvoice-v2 のみでサポートされます。

aigc_propagate_id

string

いいえ

不可視の AIGC 識別子の PropagateID フィールドを設定し、特定の伝播動作を一意に識別します。このパラメーターは enable_aigc_tagtrue の場合にのみ有効です。

デフォルト値:現在の音声合成リクエストのリクエスト ID。

この機能は cosyvoice-v3-flash、cosyvoice-v3-plus、cosyvoice-v2 のみでサポートされます。

hot_fix

object

いいえ

テキストホットパッチの構成。特定の単語の発音をカスタマイズしたり、合成前にテキストを置き換えたりできます。この機能は cosyvoice-v3-flash のクローン音声でのみ利用可能です。

パラメーター:

  • pronunciation:発音のカスタマイズ。誤ったデフォルト発音を修正するために、単語のピンインを提供します。

  • replace:テキストの置き換え。合成前に指定された単語をターゲットテキストに置き換えます。置き換えられたテキストが実際に合成されるコンテンツになります。

例:

"hot_fix": {
  "pronunciation": [
    {"weather": "tian1 qi4"}
  ],
  "replace": [
    {"today": "jin1 tian1"}
  ]
}

enable_markdown_filter

boolean

いいえ

Markdown フィルターを有効にするかどうかを指定します。有効にすると、システムが合成前に入力テキストから Markdown 記号を自動的に削除し、読み上げられることを防ぎます。この機能は cosyvoice-v3-flash のクローン音声でのみ利用可能です。

デフォルト値:false。

有効な値:

  • true

  • false

2. continue-task 命令

この命令は合成対象テキストを送信します。

すべてのテキストを 1 つの continue-task 命令で送信することも、順番に複数の continue-task 命令に分割して送信することもできます。

重要

送信タイミング: task-started イベント を受信した後。

説明

テキストフラグメントの送信間隔を 23 秒以上にしないでください。そうしないと、「request timeout after 23 seconds(23 秒後にリクエストタイムアウト)」というエラーが発生します。

これ以上テキストがない場合は、タスクを終了するために finish-task 命令 を送信してください。

サーバーは 23 秒のタイムアウトを強制します。クライアント側でこの設定を変更することはできません。

例:

{
    "header": {
        "action": "continue-task",
        "task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx", // ランダムな UUID
        "streaming": "duplex"
    },
    "payload": {
        "input": {
            "text": "Before my bed, moonlight shines bright, I suspect it's frost upon the ground."
        }
    }
}

header パラメーター参照:

パラメーター

必須

説明

header.action

string

はい

命令タイプ。

この命令では、常に "continue-task" です。

header.task_id

string

はい

このリクエストのタスク ID。

run-task 命令 で使用した task_id と一致している必要があります。

header.streaming

string

はい

固定文字列:"duplex"

payload パラメーター参照:

パラメーター

必須

説明

input.text

string

はい

合成対象テキスト。

3. finish-task 命令:タスクの終了

この命令は音声合成タスクを終了します。

必ずこの命令を送信してください。 そうしないと、以下の問題が発生する可能性があります。

  • 不完全な音声: サーバーは不完全なキャッシュされた文を強制的に合成しないため、終端部分が欠落します。

  • 接続タイムアウト: 最後の continue-task 命令 を送信してから finish-task を送信するまでの間隔が 23 秒を超えると、接続がタイムアウトして閉じられます。

  • 課金の問題: 正常に終了しないタスクは、不正確な使用量情報を返す可能性があります。

重要

送信タイミング: すべての continue-task 命令 を送信した直後に 即座に 送信してください。音声の再生完了を待ったり、送信を遅らせたりしないでください。そうするとタイムアウトが発生する可能性があります。

例:

{
    "header": {
        "action": "finish-task",
        "task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx",
        "streaming": "duplex"
    },
    "payload": {
        "input": {}//input は必須であり、省略するとエラーが返されます
    }
}

header パラメーター参照:

パラメーター

必須

説明

header.action

string

はい

命令タイプ。

この命令では、常に "finish-task" です。

header.task_id

string

はい

このリクエストのタスク ID。

run-task 命令 で使用した task_id と一致している必要があります。

header.streaming

string

はい

固定文字列:"duplex"

payload パラメーター参照:

パラメーター

必須

説明

payload.input

object

はい

固定フォーマット:{}。

イベント(サーバー → クライアント)

イベントは、サーバーからクライアントへ送信される JSON 形式のメッセージであり、それぞれが処理の段階を表します。

説明

サーバーはバイナリ音声を別途送信します。これはいかなるイベントにも含まれません。

1. task-started イベント:タスク開始

task-started イベントを受信すると、タスクが正常に開始されたことを意味します。このイベントを受信した後のみ、continue-task または finish-task 命令 を送信してください。そうしないと、タスクが失敗します。

task-started イベントの payload にはコンテンツが含まれません。

例:

{
    "header": {
        "task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx",
        "event": "task-started",
        "attributes": {}
    },
    "payload": {}
}

header パラメーター参照:

パラメーター

説明

header.event

string

イベントタイプ。

このイベントでは、常に "task-started" です。

header.task_id

string

クライアントが生成したタスク ID。

2. result-generated イベント

continue-task および finish-task 命令 を送信している間、サーバーは継続的に result-generated イベントを返します。

音声データを対応するテキストにリンクするため、サーバーは result-generated イベントとともに音声に文のメタデータを含めます。サーバーは自動的に入力テキストを文に分割します。各文の合成は、以下の 3 つのサブイベントで構成されます。

  • sentence-begin:文の開始を示し、合成対象テキストを返します。

  • sentence-synthesis:音声データチャンクを示します。各イベントの直後に、WebSocket のバイナリチャンネルを介して音声データフレームが送信されます。

    • 1 つの文は複数の sentence-synthesis イベント(チャンクごとに 1 つ)を生成します。

    • クライアントは、これらの音声チャンクを順序通りに受信し、同一のファイルに追加する必要があります。

    • sentence-synthesis イベントは、それに続く音声フレームと 1 対 1 で対応しており、ミスアライメントは発生しません。

  • sentence-end:文の終了を示し、文のテキストおよび累積課金文字数を返します。

payload.output.type フィールドを使用して、サブイベントのタイプを判別します。

例:

sentence-begin

{
    "header": {
        "task_id": "3f2d5c86-0550-45c0-801f-xxxxxxxxxx",
        "event": "result-generated",
        "attributes": {}
    },
    "payload": {
        "output": {
            "sentence": {
                "index": 0,
                "words": []
            },
            "type": "sentence-begin",
            "original_text": "Before my bed, moonlight shines bright,"
        }
    }
}

sentence-synthesis

{
    "header": {
        "task_id": "3f2d5c86-0550-45c0-801f-xxxxxxxxxx",
        "event": "result-generated",
        "attributes": {}
    },
    "payload": {
        "output": {
            "sentence": {
                "index": 0,
                "words": []
            },
            "type": "sentence-synthesis"
        }
    }
}

sentence-end

{
    "header": {
        "task_id": "3f2d5c86-0550-45c0-801f-xxxxxxxxxx",
        "event": "result-generated",
        "attributes": {}
    },
    "payload": {
        "output": {
            "sentence": {
                "index": 0,
                "words": []
            },
            "type": "sentence-end",
            "original_text": "Before my bed, moonlight shines bright,"
        },
        "usage": {
            "characters": 11
        }
    }
}

header パラメーター参照:

パラメーター

説明

header.event

string

イベントタイプ。

このイベントでは、常に "result-generated" です。

header.task_id

string

クライアントが生成したタスク ID。

header.attributes

object

追加属性(通常は空のオブジェクト)。

payload パラメーター参照:

パラメーター

説明

payload.output.type

string

サブイベントタイプ。

値:

  • sentence-begin:文の開始を示し、合成対象テキストを返します。

  • sentence-synthesis:音声データチャンクを示します。各イベントの直後に、WebSocket のバイナリチャンネルを介して音声データフレームが送信されます。

    • 1つの文は、複数の sentence-synthesis イベント——オーディオチャンクごとに1つ——を生成します。

    • クライアントは、これらのオーディオチャンクを順番に受け取り、同じファイルに追加する必要があります。

    • sentence-synthesis イベントは、その後続するオーディオフレームと 1 対 1 でマップされ、ミスアライメントは発生しません。

  • sentence-end:文末を示し、文のテキストと累積請求文字数を返します。

イベントフロー全体

合成する各文に対して、サーバーは次の順序でイベントを返します。

  1. 文の開始:文の開始を示し、文の本文(original_text)を含みます。

  2. 文の合成(複数回発生):各イベントの直後にバイナリ音声データフレームが続きます。

  3. 文の終了:文の終了を示し、文の本文および累積課金文字数を含みます。

payload.output.sentence.index

整数

文番号。0 から始まります。

payload.output.sentence.words

配列

文字に関する情報の配列です。

payload.output.sentence.words.text

文字列

単語のテキスト。

payload.output.sentence.words.begin_index

integer

単語が文内で開始する位置のインデックス(0 から数える)。

payload.output.sentence.words.end_index

整数

文中の単語の終了位置インデックス。1から数えます。

payload.output.sentence.words.begin_time

整数

単語の音声の開始タイムスタンプ (ミリ秒)。

payload.output.sentence.words.end_time

integer

単語の音声の終了タイムスタンプ、単位はミリ秒です。

payload.output.original_text

文字列

ユーザーの入力テキストを分割した後の文の内容です。最後の文では、このフィールドが省略されることがあります。

payload.usage.characters

integer

このリクエストでこれまでに課金対象となった文字数の合計です。1 つのタスク内では、usage フィールドはresult-generated イベントまたはtask-finished イベントのいずれかに出現します。usage フィールドには累積合計が含まれます。最後に出現した値を使用してください。

3. task-finished イベント:タスクの終了

task-finished イベントを受信すると、タスクは終了したことになります。

タスクが終了した後、WebSocket 接続を閉じて終了するか、接続を再利用して新しい run-task 命令を送信できます (「接続のオーバーヘッドと再利用」をご参照ください)。

例:

{
    "header": {
        "task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx",
        "event": "task-finished",
        "attributes": {
            "request_uuid": "0a9dba9e-d3a6-45a4-be6d-xxxxxxxxxxxx"
        }
    },
    "payload": {
        "output": {
            "sentence": {
                "words": []
            }
        },
        "usage": {
            "characters": 13
        }
    }
}

header パラメーターリファレンス:

パラメーター

タイプ

説明

header.event

文字列

イベントタイプ。

このイベントでは、常に "task-finished" です。

header.task_id

文字列

クライアントによって生成されたタスク ID。

header.attributes.request_uuid

文字列

リクエスト ID。問題の診断のために、この ID を CosyVoice の開発者に提供してください。

payload パラメーターリファレンス:

パラメーター

タイプ

説明

payload.usage.characters

integer

このリクエストで現在までに課金された合計文字数。 1 つのタスク内で、usage フィールドは result-generated イベント または task-finished イベント のいずれかに出現する可能性があります。usage フィールドには累計値が含まれます。最後に出現した値を使用してください。

4. task-failed イベント:タスクの失敗

task-failed イベントを受信した場合、タスクが失敗したことを示します。WebSocket 接続を閉じ、エラーを処理してください。エラーメッセージを分析し、コードの問題が原因で失敗した場合は、コードを修正してください。

例:

{
    "header": {
        "task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx",
        "event": "task-failed",
        "error_code": "InvalidParameter",
        "error_message": "[tts:]Engine return error code: 418",
        "attributes": {}
    },
    "payload": {}
}

header パラメーターリファレンス:

パラメーター

説明

header.event

文字列

イベントタイプ。

このイベントでは、常にタスクが失敗します。

header.task_id

文字列

クライアントによって生成されたタスク ID。

header.error_code

文字列

エラータイプの詳細。

header.error_message

文字列

エラーの詳細な理由。

タスク中断方法

ストリーミング合成中に、現在のタスクを早期に中断できます。たとえば、ユーザーが再生をキャンセルしたり、ライブ会話を中断したりする場合など、以下のいずれかの方法を使用します。

中断モード

サーバーの動作

ユースケース

接続を直接閉じる

  • サーバーは合成を即座に停止します。

  • すでに生成されたがまだ送信されていない音声は破棄されます。

  • クライアントは task-finished イベントを受信しません。

  • 接続は閉じられた後、再利用できません。

即時中断: ユーザーが再生をキャンセルする、コンテンツを切り替える、またはアプリを終了する。

finish-task コマンドを送信

  • サーバーはキャッシュされたすべてのテキストを強制的に合成します。

  • 残りの音声チャンクを返します。

  • task-finished イベントを返します。

  • 接続は再利用可能であり、新しいタスクを開始できます。

正常終了: 新しいテキストの送信を停止しても、キャッシュされたコンテンツの音声は受信し続けます。

接続オーバーヘッドおよび再利用

WebSocket サービスは、リソース効率を向上させ、接続設定のオーバーヘッドを回避するために、接続の再利用をサポートしています。

サーバーがクライアントから run-task 命令 を受信すると、新しいタスクを開始します。クライアントが finish-task 命令 を送信すると、タスク完了時にサーバーは task-finished イベント を返します。タスク終了後、WebSocket 接続は別の run-task 命令 を送信して次のタスクを開始することで再利用できます。

重要
  1. 新しい run-task 命令 は、サーバーが task-finished イベント を返した後のみ送信できます。

  2. 再利用された接続上の異なるタスクは、異なる task_id を使用する必要があります。

  3. タスク実行中にタスクが失敗した場合、サーバーは task-failed イベント を返し、接続を閉じます。この接続は再利用できません。

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

パフォーマンスメトリクスおよび同時実行数制限

同時実行数制限

詳細については、「レート制限」をご参照ください。

同時実行数のクォータを増やすには、たとえばより多くの同時接続をサポートするために、カスタマーサポートに連絡してください。クォータ調整には審査が必要であり、通常 1 ~ 3 営業日以内に完了します。

説明

ベストプラクティス: リソース使用効率を向上させるため、各タスクごとに新しい接続を開くのではなく、単一の WebSocket 接続を複数のタスクで再利用できます。「接続オーバーヘッドおよび再利用」をご参照ください。

接続パフォーマンスおよびレイテンシー

一般的な接続時間:

  • 中国本土のクライアント:WebSocket 接続の確立(newWebSocket から onOpen まで)は通常 200 ~ 1000 ミリ秒かかります。

  • クロスボーダー接続(香港または国際リージョンなど):接続レイテンシーは 1 ~ 3 秒に達する場合があります。まれに 10 ~ 30 秒に達することもあります。

接続時間の長期化のトラブルシューティング:

WebSocket 接続の確立に 30 秒以上かかる場合、原因は以下のいずれかの問題である可能性があります。

  1. ネットワークの問題: クライアントとサーバー間のネットワーク遅延が高い場合(クロスボーダー接続や ISP の品質の悪さによる遅延など)。

  2. DNS 解決の遅延: dashscope.aliyuncs.com の DNS 解決に時間がかかる場合。8.8.8.8 などのパブリック DNS を使用するか、ローカルホストファイルを構成してみてください。

  3. TLS ハンドシェイクの遅延: クライアントの TLS バージョンが古い場合、または証明書検証が遅い場合。TLS 1.2 以降のバージョンを使用することを推奨します。

  4. プロキシまたはファイアウォール: 企業ネットワークが WebSocket 接続をブロックしたり、プロキシの使用を要求したりする場合があります。

トラブルシューティングツール:

  • Wireshark または tcpdump を使用して、TCP ハンドシェイク、TLS ハンドシェイク、および WebSocket アップグレードフェーズのタイミングを分析できます。

  • curl を使用して HTTP 接続のレイテンシーをテストできます。curl -w "@curl-format.txt" -o /dev/null -s https://dashscope.aliyuncs.com

説明

CosyVoice WebSocket API は中国本土の北京リージョンにデプロイされています。クライアントが香港や海外リージョンなど別のリージョンにある場合、近くのリレーサーバーまたは CDN を使用して接続を高速化できます。

音声生成パフォーマンス

合成速度:

  • リアルタイムファクター(RTF):CosyVoice モデルは通常、リアルタイム速度の 0.1 ~ 0.5 倍で音声を合成します。これは、1 秒の音声を生成するのに 0.1 ~ 0.5 秒かかることを意味します。実際の速度は、モデルバージョン、テキスト長、およびサーバー負荷によって異なります。

  • 最初のパケットのレイテンシー:continue-task 命令を送信してから最初の音声チャンクを受信するまでのレイテンシーは、通常 200 ~ 800 ミリ秒です。

サンプルコード

このサンプルコードは基本的なサービス接続のみを示しています。特定のユースケースに合わせて、本番環境に対応したロジックを実装する必要があります。

WebSocket クライアントコードを作成する際は、非同期プログラミングを使用してメッセージを同時に送受信してください。以下の手順に従います。

  1. WebSocket 接続の確立

    WebSocket ライブラリの接続関数を呼び出します(実装は言語またはライブラリによって異なります)。ヘッダー および URL を渡します。

  2. サーバーメッセージのリッスン

    WebSocket ライブラリのコールバック関数(オブザーバーパターンに基づく)を使用して、サーバーメッセージをリッスンします。実装は言語によって異なります。

    サーバーメッセージは、バイナリ音声ストリームと イベント の 2 つのカテゴリに分類されます。

    リッスン対象 イベント

    バイナリ音声ストリームの処理: サーバーは binary チャンネルを介してフレーム単位で音声を送信します。完全な音声データは複数のパケットに分割されます。

    • ストリーミング音声合成において、MP3 や Opus などの圧縮フォーマットの場合、分割された音声データはストリーミングプレーヤーを使用して再生する必要があります。フレームごとに再生するとデコードが失敗するため、行わないでください。

      ストリーミングプレーヤーには、FFmpeg、PyAudio(Python)、AudioFormat(Java)、MediaSource(JavaScript)などがあります。
    • 音声データを完全な音声ファイルに結合する場合、追加モードで同じファイルに書き込みます。

    • ストリーミング音声合成からの WAV および MP3 音声の場合、最初のフレームのみがヘッダー情報を含みます。後続のフレームは音声データのみを含みます。

  3. サーバーへのメッセージ送信(タイミングに注意)

    サーバーメッセージをリッスンするスレッドとは別のスレッド(例:メインスレッド)から、サーバーに命令を送信します。実装は言語によって異なります。

    命令は厳密な順序で送信してください。そうでないとタスクが失敗する可能性があります。

    1. run-task 命令 を送信

      • 音声合成タスクを開始します。

      • 返される task_id は、その後の continue-task および finish-task 命令 で一貫して使用する必要があります。

    2. continue-task 命令 を送信

      • 合成対象テキストを送信します。

      • task-started イベント をサーバーから受信した後のみ送信してください。

    3. finish-task 命令 を送信

      • 音声合成タスクを終了します。

      • すべての continue-task 命令 を送信した後に送信してください。

  4. WebSocket 接続を閉じる

    プログラムが正常に終了したとき、例外が発生したとき、または task-finished イベント または task-failed イベント を受信したときに、WebSocket 接続を閉じます。通常はライブラリの close 関数を呼び出します。

完全な例を見る

Go

package main

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

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

const (
	// Use this URL for Singapore region. For Beijing region, replace with: wss://dashscope.aliyuncs.com/api-ws/v1/inference/
	wsURL      = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/inference/"
	outputFile = "output.mp3"
)

func main() {
	// Get API key: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
	// If no environment variable is set, replace next line with: apiKey := "sk-xxx"
	apiKey := os.Getenv("DASHSCOPE_API_KEY")

	// Clear output file
	os.Remove(outputFile)
	os.Create(outputFile)

	// Connect WebSocket
	header := make(http.Header)
	header.Add("X-DashScope-DataInspection", "enable")
	header.Add("Authorization", fmt.Sprintf("bearer %s", apiKey))

	conn, resp, err := websocket.DefaultDialer.Dial(wsURL, header)
	if err != nil {
		if resp != nil {
			fmt.Printf("Connection failed HTTP status code: %d\n", resp.StatusCode)
		}
		fmt.Println("Connection failed:", err)
		return
	}
	defer conn.Close()

	// Generate task ID
	taskID := uuid.New().String()
	fmt.Printf("Generated task ID: %s\n", taskID)

	// Send run-task instruction
	runTaskCmd := map[string]interface{}{
		"header": map[string]interface{}{
			"action":    "run-task",
			"task_id":   taskID,
			"streaming": "duplex",
		},
		"payload": map[string]interface{}{
			"task_group": "audio",
			"task":       "tts",
			"function":   "SpeechSynthesizer",
			"model":      "cosyvoice-v3-flash",
			"parameters": map[string]interface{}{
				"text_type":   "PlainText",
				"voice":       "longanyang",
				"format":      "mp3",
				"sample_rate": 22050,
				"volume":      50,
				"rate":        1,
				"pitch":       1,
				// If enable_ssml is true, only one continue-task instruction is allowed. Otherwise, it returns “Text request limit violated, expected 1.”
				"enable_ssml": false,
			},
			"input": map[string]interface{}{},
		},
	}

	runTaskJSON, _ := json.Marshal(runTaskCmd)
	fmt.Printf("Sent run-task instruction: %s\n", string(runTaskJSON))

	err = conn.WriteMessage(websocket.TextMessage, runTaskJSON)
	if err != nil {
		fmt.Println("Failed to send run-task:", err)
		return
	}

	textSent := false

	// Process messages
	for {
		messageType, message, err := conn.ReadMessage()
		if err != nil {
			fmt.Println("Failed to read message:", err)
			break
		}

		// Process binary message
		if messageType == websocket.BinaryMessage {
			fmt.Printf("Received binary message, length: %d\n", len(message))
			file, _ := os.OpenFile(outputFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
			file.Write(message)
			file.Close()
			continue
		}

		// Process text message
		messageStr := string(message)
		fmt.Printf("Received text message: %s\n", strings.ReplaceAll(messageStr, "\n", ""))

		// Simple JSON parse to get event type
		var msgMap map[string]interface{}
		if json.Unmarshal(message, &msgMap) == nil {
			if header, ok := msgMap["header"].(map[string]interface{}); ok {
				if event, ok := header["event"].(string); ok {
					fmt.Printf("Event type: %s\n", event)

					switch event {
					case "task-started":
						fmt.Println("=== Received task-started event ===")

						if !textSent {
							// Send continue-task instruction

							texts := []string{"Before my bed, moonlight shines bright, I suspect it's frost upon the ground.", "I raise my eyes to gaze at the bright moon, then bow my head, thinking of home."}

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

								continueTaskJSON, _ := json.Marshal(continueTaskCmd)
								fmt.Printf("Sent continue-task instruction: %s\n", string(continueTaskJSON))

								err = conn.WriteMessage(websocket.TextMessage, continueTaskJSON)
								if err != nil {
									fmt.Println("Failed to send continue-task:", err)
									return
								}
							}

							textSent = true

							// Delay before sending finish-task
							time.Sleep(500 * time.Millisecond)

							// Send finish-task instruction
							finishTaskCmd := map[string]interface{}{
								"header": map[string]interface{}{
									"action":    "finish-task",
									"task_id":   taskID,
									"streaming": "duplex",
								},
								"payload": map[string]interface{}{
									"input": map[string]interface{}{},
								},
							}

							finishTaskJSON, _ := json.Marshal(finishTaskCmd)
							fmt.Printf("Sent finish-task instruction: %s\n", string(finishTaskJSON))

							err = conn.WriteMessage(websocket.TextMessage, finishTaskJSON)
							if err != nil {
								fmt.Println("Failed to send finish-task:", err)
								return
							}
						}

					case "task-finished":
						fmt.Println("=== Task completed ===")
						return

					case "task-failed":
						fmt.Println("=== Task failed ===")
						if header["error_message"] != nil {
							fmt.Printf("Error message: %s\n", header["error_message"])
						}
						return

					case "result-generated":
						fmt.Println("Received result-generated event")
					}
				}
			}
		}
	}
}

C#

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

class Program {
    // Get API key: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
    // If no environment variable is set, replace next line with: private static readonly string ApiKey = "sk-xxx"
    private static readonly string ApiKey = Environment.GetEnvironmentVariable("DASHSCOPE_API_KEY") ?? throw new InvalidOperationException("DASHSCOPE_API_KEY environment variable is not set.");

    // Use this URL for Singapore region. For Beijing region, replace with: wss://dashscope.aliyuncs.com/api-ws/v1/inference/
    private const string WebSocketUrl = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/inference/";
    // Output file path
    private const string OutputFilePath = "output.mp3";

    // WebSocket client
    private static ClientWebSocket _webSocket = new ClientWebSocket();
    // Cancellation token source
    private static CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
    // Task ID
    private static string? _taskId;
    // Task started flag
    private static TaskCompletionSource<bool> _taskStartedTcs = new TaskCompletionSource<bool>();

    static async Task Main(string[] args) {
        try {
            // Clear output file
            ClearOutputFile(OutputFilePath);

            // Connect to WebSocket service
            await ConnectToWebSocketAsync(WebSocketUrl);

            // Start receiving messages
            Task receiveTask = ReceiveMessagesAsync();

            // Send run-task instruction
            _taskId = GenerateTaskId();
            await SendRunTaskCommandAsync(_taskId);

            // Wait for task-started event
            await _taskStartedTcs.Task;

            // Send continue-task instructions
            string[] texts = {
                "Before my bed, moonlight shines bright",
                "I suspect it's frost upon the ground",
                "I raise my eyes to gaze at the bright moon",
                "then bow my head, thinking of home"
            };
            foreach (string text in texts) {
                await SendContinueTaskCommandAsync(text);
            }

            // Send finish-task instruction
            await SendFinishTaskCommandAsync(_taskId);

            // Wait for receive task to complete
            await receiveTask;

            Console.WriteLine("Task completed. Connection closed.");
        } catch (OperationCanceledException) {
            Console.WriteLine("Task canceled.");
        } catch (Exception ex) {
            Console.WriteLine($"Error: {ex.Message}");
        } finally {
            _cancellationTokenSource.Cancel();
            _webSocket.Dispose();
        }
    }

    private static void ClearOutputFile(string filePath) {
        if (File.Exists(filePath)) {
            File.WriteAllText(filePath, string.Empty);
            Console.WriteLine("Output file cleared.");
        } else {
            Console.WriteLine("Output file does not exist. No action needed.");
        }
    }

    private static async Task ConnectToWebSocketAsync(string url) {
        var uri = new Uri(url);
        if (_webSocket.State == WebSocketState.Connecting || _webSocket.State == WebSocketState.Open) {
            return;
        }

        // Set WebSocket request headers
        _webSocket.Options.SetRequestHeader("Authorization", $"bearer {ApiKey}");
        _webSocket.Options.SetRequestHeader("X-DashScope-DataInspection", "enable");

        try {
            await _webSocket.ConnectAsync(uri, _cancellationTokenSource.Token);
            Console.WriteLine("Successfully connected to WebSocket service.");
        } catch (OperationCanceledException) {
            Console.WriteLine("WebSocket connection canceled.");
        } catch (Exception ex) {
            Console.WriteLine($"WebSocket connection failed: {ex.Message}");
            throw;
        }
    }

    private static async Task SendRunTaskCommandAsync(string taskId) {
        var command = CreateCommand("run-task", taskId, "duplex", new {
            task_group = "audio",
            task = "tts",
            function = "SpeechSynthesizer",
            model = "cosyvoice-v3-flash",
            parameters = new
            {
                text_type = "PlainText",
                voice = "longanyang",
                format = "mp3",
                sample_rate = 22050,
                volume = 50,
                rate = 1,
                pitch = 1,
                // If enable_ssml is true, only one continue-task instruction is allowed. Otherwise, it returns “Text request limit violated, expected 1.”
                enable_ssml = false
            },
            input = new { }
        });

        await SendJsonMessageAsync(command);
        Console.WriteLine("Sent run-task instruction.");
    }

    private static async Task SendContinueTaskCommandAsync(string text) {
        if (_taskId == null) {
            throw new InvalidOperationException("Task ID not initialized.");
        }

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

        await SendJsonMessageAsync(command);
        Console.WriteLine("Sent continue-task instruction.");
    }

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

        await SendJsonMessageAsync(command);
        Console.WriteLine("Sent finish-task instruction.");
    }

    private static async Task SendJsonMessageAsync(string message) {
        var buffer = Encoding.UTF8.GetBytes(message);
        try {
            await _webSocket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, _cancellationTokenSource.Token);
        } catch (OperationCanceledException) {
            Console.WriteLine("Message send canceled.");
        }
    }

    private static async Task ReceiveMessagesAsync() {
        while (_webSocket.State == WebSocketState.Open) {
            var response = await ReceiveMessageAsync();
            if (response != null) {
                var eventStr = response.RootElement.GetProperty("header").GetProperty("event").GetString();
                switch (eventStr) {
                    case "task-started":
                        Console.WriteLine("Task started.");
                        _taskStartedTcs.TrySetResult(true);
                        break;
                    case "task-finished":
                        Console.WriteLine("Task completed.");
                        _cancellationTokenSource.Cancel();
                        break;
                    case "task-failed":
                        Console.WriteLine("Task failed: " + response.RootElement.GetProperty("header").GetProperty("error_message").GetString());
                        _cancellationTokenSource.Cancel();
                        break;
                    default:
                        // Handle result-generated here
                        break;
                }
            }
        }
    }

    private static async Task<JsonDocument?> ReceiveMessageAsync() {
        var buffer = new byte[1024 * 4];
        var segment = new ArraySegment<byte>(buffer);

        try {
            WebSocketReceiveResult result = await _webSocket.ReceiveAsync(segment, _cancellationTokenSource.Token);

            if (result.MessageType == WebSocketMessageType.Close) {
                await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", _cancellationTokenSource.Token);
                return null;
            }

            if (result.MessageType == WebSocketMessageType.Binary) {
                // Process binary data
                Console.WriteLine("Received binary data...");

                // Save binary data to file
                using (var fileStream = new FileStream(OutputFilePath, FileMode.Append)) {
                    fileStream.Write(buffer, 0, result.Count);
                }

                return null;
            }

            string message = Encoding.UTF8.GetString(buffer, 0, result.Count);
            return JsonDocument.Parse(message);
        } catch (OperationCanceledException) {
            Console.WriteLine("Message receive canceled.");
            return null;
        }
    }

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

    private static string CreateCommand(string action, string taskId, string streaming, object payload) {
        var command = new {
            header = new {
                action,
                task_id = taskId,
                streaming
            },
            payload
        };

        return JsonSerializer.Serialize(command);
    }
}

PHP

サンプルコードのディレクトリ構造:

my-php-project/

├── composer.json

├── vendor/

└── index.php

composer.json の内容(必要に応じてバージョンを調整):

{
    "require": {
        "react/event-loop": "^1.3",
        "react/socket": "^1.11",
        "react/stream": "^1.2",
        "react/http": "^1.1",
        "ratchet/pawl": "^0.4"
    },
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    }
}

index.php の内容:

<?php

require __DIR__ . '/vendor/autoload.php';

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

// Get API key: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// If no environment variable is set, replace next line with: $api_key = "sk-xxx"
$api_key = getenv("DASHSCOPE_API_KEY");
// Use this URL for Singapore region. For Beijing region, replace with: wss://dashscope.aliyuncs.com/api-ws/v1/inference/
$websocket_url = 'wss://dashscope-intl.aliyuncs.com/api-ws/v1/inference/'; // WebSocket サーバーアドレス
$output_file = 'output.mp3'; // 出力ファイルパス

$loop = Loop::get();

if (file_exists($output_file)) {
    // ファイルコンテンツをクリア
    file_put_contents($output_file, '');
}

// カスタムコネクタを作成
$socketConnector = new SocketConnector($loop, [
    'tcp' => [
        'bindto' => '0.0.0.0:0',
    ],
    'tls' => [
        'verify_peer' => false,
        'verify_peer_name' => false,
    ],
]);

$connector = new Connector($loop, $socketConnector);

$headers = [
    'Authorization' => 'bearer ' . $api_key,
    'X-DashScope-DataInspection' => 'enable'
];

$connector($websocket_url, [], $headers)->then(function ($conn) use ($loop, $output_file) {
    echo "Connected to WebSocket server\n";

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

    // run-task 命令を送信
    sendRunTaskMessage($conn, $taskId);

    // continue-task 命令を送信する関数を定義
    $sendContinueTask = function() use ($conn, $loop, $taskId) {
        // 送信するテキスト
        $texts = ["Before my bed, moonlight shines bright", "I suspect it's frost upon the ground", "I raise my eyes to gaze at the bright moon", "then bow my head, thinking of home"];
        $continueTaskCount = 0;
        foreach ($texts as $text) {
            $continueTaskMessage = json_encode([
                "header" => [
                    "action" => "continue-task",
                    "task_id" => $taskId,
                    "streaming" => "duplex"
                ],
                "payload" => [
                    "input" => [
                        "text" => $text
                    ]
                ]
            ]);
            echo "Preparing to send continue-task instruction: " . $continueTaskMessage . "\n";
            $conn->send($continueTaskMessage);
            $continueTaskCount++;
        }
        echo "Number of continue-task instructions sent: " . $continueTaskCount . "\n";

        // finish-task 命令を送信
        sendFinishTaskMessage($conn, $taskId);
    };

    // task-started イベントのフラグ
    $taskStarted = false;

    // メッセージをリッスン
    $conn->on('message', function($msg) use ($conn, $sendContinueTask, $loop, &$taskStarted, $taskId, $output_file) {
        if ($msg->isBinary()) {
            // バイナリデータをローカルファイルに書き込み
            file_put_contents($output_file, $msg->getPayload(), FILE_APPEND);
        } else {
            // 非バイナリメッセージを処理
            $response = json_decode($msg, true);

            if (isset($response['header']['event'])) {
                handleEvent($conn, $response, $sendContinueTask, $loop, $taskId, $taskStarted);
            } else {
                echo "Unknown message format\n";
            }
        }
    });

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

$loop->run();

/**
 * タスク ID を生成
 * @return string
 */
function generateTaskId(): string {
    return bin2hex(random_bytes(16));
}

/**
 * run-task 命令を送信
 * @param $conn
 * @param $taskId
 */
function sendRunTaskMessage($conn, $taskId) {
    $runTaskMessage = json_encode([
        "header" => [
            "action" => "run-task",
            "task_id" => $taskId,
            "streaming" => "duplex"
        ],
        "payload" => [
            "task_group" => "audio",
            "task" => "tts",
            "function" => "SpeechSynthesizer",
            "model" => "cosyvoice-v3-flash",
            "parameters" => [
                "text_type" => "PlainText",
                "voice" => "longanyang",
                "format" => "mp3",
                "sample_rate" => 22050,
                "volume" => 50,
                "rate" => 1,
                "pitch" => 1,
                // If enable_ssml is true, only one continue-task instruction is allowed. Otherwise, it returns “Text request limit violated, expected 1.”
                "enable_ssml" => false
            ],
            "input" => (object) []
        ]
    ]);
    echo "Preparing to send run-task instruction: " . $runTaskMessage . "\n";
    $conn->send($runTaskMessage);
    echo "run-task instruction sent\n";
}

/**
 * 音声ファイルを読み込み
 * @param string $filePath
 * @return bool|string
 */
function readAudioFile(string $filePath) {
    $voiceData = file_get_contents($filePath);
    if ($voiceData === false) {
        echo "Cannot read audio file\n";
    }
    return $voiceData;
}

/**
 * 音声データを分割
 * @param string $data
 * @param int $chunkSize
 * @return array
 */
function splitAudioData(string $data, int $chunkSize): array {
    return str_split($data, $chunkSize);
}

/**
 * finish-task 命令を送信
 * @param $conn
 * @param $taskId
 */
function sendFinishTaskMessage($conn, $taskId) {
    $finishTaskMessage = json_encode([
        "header" => [
            "action" => "finish-task",
            "task_id" => $taskId,
            "streaming" => "duplex"
        ],
        "payload" => [
            "input" => (object) []
        ]
    ]);
    echo "Preparing to send finish-task instruction: " . $finishTaskMessage . "\n";
    $conn->send($finishTaskMessage);
    echo "finish-task instruction sent\n";
}

/**
 * イベントを処理
 * @param $conn
 * @param $response
 * @param $sendContinueTask
 * @param $loop
 * @param $taskId
 * @param $taskStarted
 */
function handleEvent($conn, $response, $sendContinueTask, $loop, $taskId, &$taskStarted) {
    switch ($response['header']['event']) {
        case 'task-started':
            echo "Task started. Sending continue-task instructions...\n";
            $taskStarted = true;
            // continue-task 命令を送信
            $sendContinueTask();
            break;
        case 'result-generated':
            // result-generated イベントを受信
            break;
        case 'task-finished':
            echo "Task completed\n";
            $conn->close();
            break;
        case 'task-failed':
            echo "Task failed\n";
            echo "Error code: " . $response['header']['error_code'] . "\n";
            echo "Error message: " . $response['header']['error_message'] . "\n";
            $conn->close();
            break;
        case 'error':
            echo "Error: " . $response['payload']['message'] . "\n";
            break;
        default:
            echo "Unknown event: " . $response['header']['event'] . "\n";
            break;
    }

    // タスクが完了した場合、接続を閉じる
    if ($response['header']['event'] == 'task-finished') {
        // すべてのデータが送信されることを保証するために 1 秒待機
        $loop->addTimer(1, function() use ($conn) {
            $conn->close();
            echo "Client closed connection\n";
        });
    }

    // task-started イベントが受信されなかった場合、接続を閉じる
    if (!$taskStarted && in_array($response['header']['event'], ['task-failed', 'error'])) {
        $conn->close();
    }
}

Node.js

依存関係をインストール:

npm install ws
npm install uuid

サンプルコード:

const WebSocket = require('ws');
const fs = require('fs');
const uuid = require('uuid').v4;

// Get API key: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// If no environment variable is set, replace next line with: const apiKey = "sk-xxx"
const apiKey = process.env.DASHSCOPE_API_KEY;
// Use this URL for Singapore region. For Beijing region, replace with: wss://dashscope-intl.aliyuncs.com/api-ws/v1/inference/
const url = 'wss://dashscope-intl.aliyuncs.com/api-ws/v1/inference/';
// Output file path
const outputFilePath = 'output.mp3';

// Clear output file
fs.writeFileSync(outputFilePath, '');

// Create WebSocket client
const ws = new WebSocket(url, {
  headers: {
    Authorization: `bearer ${apiKey}`,
    'X-DashScope-DataInspection': 'enable'
  }
});

let taskStarted = false;
let taskId = uuid();

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

  // Send run-task instruction
  const runTaskMessage = JSON.stringify({
    header: {
      action: 'run-task',
      task_id: taskId,
      streaming: 'duplex'
    },
    payload: {
      task_group: 'audio',
      task: 'tts',
      function: 'SpeechSynthesizer',
      model: 'cosyvoice-v3-flash',
      parameters: {
        text_type: 'PlainText',
        voice: 'longanyang', // 音声
        format: 'mp3', // 音声フォーマット
        sample_rate: 22050, // サンプルレート
        volume: 50, // ボリューム
        rate: 1, // 話速
        pitch: 1, // ピッチ
        enable_ssml: false // SSML を有効化。true の場合、continue-task 命令は 1 つのみ許可されます。そうでない場合、「Text request limit violated, expected 1.」が返されます。
      },
      input: {}
    }
  });
  ws.send(runTaskMessage);
  console.log('Sent run-task message');
});

const fileStream = fs.createWriteStream(outputFilePath, { flags: 'a' });
ws.on('message', (data, isBinary) => {
  if (isBinary) {
    // バイナリデータをファイルに書き込み
    fileStream.write(data);
  } else {
    const message = JSON.parse(data);

    switch (message.header.event) {
      case 'task-started':
        taskStarted = true;
        console.log('Task started');
        // continue-task 命令を送信
        sendContinueTasks(ws);
        break;
      case 'task-finished':
        console.log('Task completed');
        ws.close();
        fileStream.end(() => {
          console.log('File stream closed');
        });
        break;
      case 'task-failed':
        console.error('Task failed: ', message.header.error_message);
        ws.close();
        fileStream.end(() => {
          console.log('File stream closed');
        });
        break;
      default:
        // result-generated をここで処理
        break;
    }
  }
});

function sendContinueTasks(ws) {
  const texts = [
    'Before my bed, moonlight shines bright,',
    'I suspect it\'s frost upon the ground.',
    'I raise my eyes to gaze at the bright moon,',
    'then bow my head, thinking of home.'
  ];

  texts.forEach((text, index) => {
    setTimeout(() => {
      if (taskStarted) {
        const continueTaskMessage = JSON.stringify({
          header: {
            action: 'continue-task',
            task_id: taskId,
            streaming: 'duplex'
          },
          payload: {
            input: {
              text: text
            }
          }
        });
        ws.send(continueTaskMessage);
        console.log(`Sent continue-task, text: ${text}`);
      }
    }, index * 1000); // 1 秒ごとに送信
  });

  // finish-task 命令を送信
  setTimeout(() => {
    if (taskStarted) {
      const finishTaskMessage = JSON.stringify({
        header: {
          action: 'finish-task',
          task_id: taskId,
          streaming: 'duplex'
        },
        payload: {
          input: {}
        }
      });
      ws.send(finishTaskMessage);
      console.log('Sent finish-task');
    }
  }, texts.length * 1000 + 1000); // 最後の continue-task から 1 秒後に送信
}

ws.on('close', () => {
  console.log('Disconnected from WebSocket server');
});

Java

Java を使用する場合、Java DashScope SDK の使用を推奨します。「Java SDK」をご参照ください。

以下は Java WebSocket の使用例です。実行前に、以下の依存関係をインポートしてください。

  • Java-WebSocket

  • jackson-databind

Maven または Gradle で依存関係を管理することを推奨します。構成例:

pom.xml

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

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

build.gradle

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

Java コード:

import com.fasterxml.jackson.databind.ObjectMapper;

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

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

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

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

    @Override
    public void onOpen(ServerHandshake serverHandshake) {
        System.out.println("Connection successful");

        // Send run-task instruction
        // If enable_ssml is true, only one continue-task instruction is allowed. Otherwise, it returns “Text request limit violated, expected 1.”
        String runTaskCommand = "{ \"header\": { \"action\": \"run-task\", \"task_id\": \"" + taskId + "\", \"streaming\": \"duplex\" }, \"payload\": { \"task_group\": \"audio\", \"task\": \"tts\", \"function\": \"SpeechSynthesizer\", \"model\": \"cosyvoice-v3-flash\", \"parameters\": { \"text_type\": \"PlainText\", \"voice\": \"longanyang\", \"format\": \"mp3\", \"sample_rate\": 22050, \"volume\": 50, \"rate\": 1, \"pitch\": 1, \"enable_ssml\": false }, \"input\": {} }}";
        send(runTaskCommand);
    }

    @Override
    public void onMessage(String message) {
        System.out.println("Received server message: " + message);
        try {
            // Parse JSON message
            Map<String, Object> messageMap = new ObjectMapper().readValue(message, Map.class);

            if (messageMap.containsKey("header")) {
                Map<String, Object> header = (Map<String, Object>) messageMap.get("header");

                if (header.containsKey("event")) {
                    String event = (String) header.get("event");

                    if ("task-started".equals(event)) {
                        System.out.println("Received task-started event");

                        List<String> texts = Arrays.asList(
                                "Before my bed, moonlight shines bright, I suspect it's frost upon the ground",
                                "I raise my eyes to gaze at the bright moon, then bow my head, thinking of home"
                        );

                        for (String text : texts) {
                            // Send continue-task instruction
                            sendContinueTask(text);
                        }

                        // Send finish-task instruction
                        sendFinishTask();
                    } else if ("task-finished".equals(event)) {
                        System.out.println("Received task-finished event");
                        taskFinished = true;
                        closeConnection();
                    } else if ("task-failed".equals(event)) {
                        System.out.println("Task failed: " + message);
                        closeConnection();
                    }
                }
            }
        } catch (Exception e) {
            System.err.println("Exception occurred: " + e.getMessage());
        }
    }

    @Override
    public void onMessage(ByteBuffer message) {
        System.out.println("Received binary audio data size: " + message.remaining());

        try (FileOutputStream fos = new FileOutputStream(outputFile, true)) {
            byte[] buffer = new byte[message.remaining()];
            message.get(buffer);
            fos.write(buffer);
            System.out.println("Audio data written to local file " + outputFile);
        } catch (IOException e) {
            System.err.println("Failed to write audio data to local file: " + e.getMessage());
        }
    }

    @Override
    public void onClose(int code, String reason, boolean remote) {
        System.out.println("Connection closed: " + reason + " (" + code + ")");
    }

    @Override
    public void onError(Exception ex) {
        System.err.println("Error: " + ex.getMessage());
        ex.printStackTrace();
    }

    private void sendContinueTask(String text) {
        String command = "{ \"header\": { \"action\": \"continue-task\", \"task_id\": \"" + taskId + "\", \"streaming\": \"duplex\" }, \"payload\": { \"input\": { \"text\": \"" + text + "\" } }}";
        send(command);
    }

    private void sendFinishTask() {
        String command = "{ \"header\": { \"action\": \"finish-task\", \"task_id\": \"" + taskId + "\", \"streaming\": \"duplex\" }, \"payload\": { \"input\": {} }}";
        send(command);
    }

    private void closeConnection() {
        if (!isClosed()) {
            close();
        }
    }

    public static void main(String[] args) {
        try {
            // Get API key: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
            // If no environment variable is set, replace next line with: String apiKey = "sk-xxx"
            String apiKey = System.getenv("DASHSCOPE_API_KEY");
            if (apiKey == null || apiKey.isEmpty()) {
                System.err.println("Set DASHSCOPE_API_KEY environment variable");
                return;
            }

            Map<String, String> headers = new HashMap<>();
            headers.put("Authorization", "bearer " + apiKey);
            // Use this URL for Singapore region. For Beijing region, replace with: wss://dashscope-intl.aliyuncs.com/api-ws/v1/inference/
            TTSWebSocketClient client = new TTSWebSocketClient(new URI("wss://dashscope-intl.aliyuncs.com/api-ws/v1/inference/"), headers);

            client.connect();

            while (!client.isClosed() && !client.taskFinished) {
                Thread.sleep(1000);
            }
        } catch (Exception e) {
            System.err.println("Failed to connect to WebSocket service: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

Python

Python を使用する場合、Python DashScope SDK の使用を推奨します。「Python SDK」をご参照ください。

以下は Python WebSocket の使用例です。実行前に、依存関係をインストールしてください。

pip uninstall websocket-client
pip uninstall websocket
pip install websocket-client
重要

Python スクリプトに「websocket.py」という名前を付けないでください。そうしないと、AttributeError: module 'websocket' has no attribute 'WebSocketApp'. Did you mean: 'WebSocket'? が発生します。

import websocket
import json
import uuid
import os
import time


class TTSClient:
    def __init__(self, api_key, uri):
        """
    TTSClient インスタンスを初期化

    パラメーター:
        api_key (str): 認証用の API キー
        uri (str): WebSocket サービスアドレス
    """
        self.api_key = api_key  # API キーに置き換え
        self.uri = uri  # WebSocket アドレスに置き換え
        self.task_id = str(uuid.uuid4())  # 一意のタスク ID を生成
        self.output_file = f"output_{int(time.time())}.mp3"  # 出力音声ファイルパス
        self.ws = None  # WebSocketApp インスタンス
        self.task_started = False  # task-started が受信されたかどうか
        self.task_finished = False  # task-finished または task-failed が受信されたかどうか

    def on_open(self, ws):
        """
    WebSocket 接続が開いたときのコールバック
    音声合成を開始するために run-task 命令を送信
    """
        print("WebSocket connected")

        # run-task 命令を構築
        run_task_cmd = {
            "header": {
                "action": "run-task",
                "task_id": self.task_id,
                "streaming": "duplex"
            },
            "payload": {
                "task_group": "audio",
                "task": "tts",
                "function": "SpeechSynthesizer",
                "model": "cosyvoice-v3-flash",
                "parameters": {
                    "text_type": "PlainText",
                    "voice": "longanyang",
                    "format": "mp3",
                    "sample_rate": 22050,
                    "volume": 50,
                    "rate": 1,
                    "pitch": 1,
                    # enable_ssml が True の場合、continue-task 命令は 1 つのみ許可されます。そうでない場合、「Text request limit violated, expected 1.」が返されます。
                    "enable_ssml": False
                },
                "input": {}
            }
        }

        # run-task 命令を送信
        ws.send(json.dumps(run_task_cmd))
        print("Sent run-task instruction")

    def on_message(self, ws, message):
        """
    メッセージ受信時のコールバック
    テキストメッセージとバイナリメッセージを個別に処理
    """
        if isinstance(message, str):
            # JSON テキストメッセージを処理
            try:
                msg_json = json.loads(message)
                print(f"Received JSON message: {msg_json}")

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

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

                        if event == "task-started":
                            print("Task started")
                            self.task_started = True

                            # continue-task 命令を送信
                            texts = [
                                "Before my bed, moonlight shines bright, I suspect it's frost upon the ground",
                                "I raise my eyes to gaze at the bright moon, then bow my head, thinking of home"
                            ]

                            for text in texts:
                                self.send_continue_task(text)

                            # すべての continue-task 命令の後に finish-task を送信
                            self.send_finish_task()

                        elif event == "task-finished":
                            print("Task completed")
                            self.task_finished = True
                            self.close(ws)

                        elif event == "task-failed":
                            error_msg = msg_json.get("error_message", "Unknown error")
                            print(f"Task failed: {error_msg}")
                            self.task_finished = True
                            self.close(ws)

            except json.JSONDecodeError as e:
                print(f"JSON parsing failed: {e}")
        else:
            # バイナリメッセージ(音声データ)を処理
            print(f"Received binary message, size: {len(message)} bytes")
            with open(self.output_file, "ab") as f:
                f.write(message)
            print(f"Audio data written to local file {self.output_file}")

    def on_error(self, ws, error):
        """エラー時のコールバック"""
        print(f"WebSocket error: {error}")

    def on_close(self, ws, close_status_code, close_msg):
        """クローズ時のコールバック"""
        print(f"WebSocket closed: {close_msg} ({close_status_code})")

    def send_continue_task(self, text):
        """合成対象テキストを含む continue-task 命令を送信"""
        cmd = {
            "header": {
                "action": "continue-task",
                "task_id": self.task_id,
                "streaming": "duplex"
            },
            "payload": {
                "input": {
                    "text": text
                }
            }
        }

        self.ws.send(json.dumps(cmd))
        print(f"Sent continue-task instruction, text: {text}")

    def send_finish_task(self):
        """音声合成を終了するために finish-task 命令を送信"""
        cmd = {
            "header": {
                "action": "finish-task",
                "task_id": self.task_id,
                "streaming": "duplex"
            },
            "payload": {
                "input": {}
            }
        }

        self.ws.send(json.dumps(cmd))
        print("Sent finish-task instruction")

    def close(self, ws):
        """接続を手動で閉じる"""
        if ws and ws.sock and ws.sock.connected:
            ws.close()
            print("Manually closed connection")

    def run(self):
        """WebSocket クライアントを開始"""
        # リクエストヘッダー(認証)を設定
        header = {
            "Authorization": f"bearer {self.api_key}",
            "X-DashScope-DataInspection": "enable"
        }

        # WebSocketApp インスタンスを作成
        self.ws = websocket.WebSocketApp(
            self.uri,
            header=header,
            on_open=self.on_open,
            on_message=self.on_message,
            on_error=self.on_error,
            on_close=self.on_close
        )

        print("Listening for WebSocket messages...")
        self.ws.run_forever()  # 長期接続を開始


# 使用例
if __name__ == "__main__":
    # Get API key: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
    # If no environment variable is set, replace next line with: API_KEY = "sk-xxx"
    API_KEY = os.environ.get("DASHSCOPE_API_KEY")
    # Use this URL for Singapore region. For Beijing region, replace with: wss://dashscope.aliyuncs.com/api-ws/v1/inference/
    SERVER_URI = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/inference/"  # WebSocket アドレスに置き換え

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

エラーコード

エラーが発生した場合、トラブルシューティングについては「エラーメッセージ」をご参照ください。

よくある質問

機能、課金、およびレート制限

Q:発音が不正確な場合、どうすればよいですか?

SSML を使用して、音声合成効果をカスタマイズできます。

Q:なぜ HTTP/HTTPS ではなく WebSocket を使用するのですか? RESTful API を提供しないのはなぜですか?

音声サービスが HTTP/HTTPS や RESTful API ではなく WebSocket を使用するのは、全二重通信が必要だからです。WebSocket は、サーバーとクライアントの両方が合成や認識のリアルタイム進捗更新などのデータを能動的にプッシュすることを可能にします。HTTP 上の RESTful API はクライアントが開始するリクエスト・レスポンスサイクルのみをサポートしており、リアルタイムのインタラクション要件を満たすことができません。

Q:音声合成は文字数単位で課金されます。各合成の文字数を確認または取得するにはどうすればよいですか?

サーバーの result-generated イベント 内の payload.usage.characters パラメーターから文字数を取得できます。最後に受信した result-generated イベントの値を使用してください。

トラブルシューティング

重要

コードでエラーが発生した場合、サーバーに送信された 命令 が正しいかどうかを確認してください。命令を印刷し、そのフォーマットと必須フィールドを検証してください。命令が正しい場合は、さらなる診断のために エラーコード を参照してください。

Q:リクエスト ID を取得するにはどうすればよいですか?

以下の 2 つの方法で取得できます。

Q:SSML が失敗するのはなぜですか?

この問題を段階的にトラブルシューティングしてください。

  1. 制限事項 に正しく従っていることを確認してください。

  2. SSML を正しく呼び出していることを確認してください。詳細については、「SSML マークアップ言語のサポート」をご参照ください。

  3. テキストがプレーンテキストであり、フォーマット要件を満たしていることを確認してください。詳細については、「SSML マークアップ言語の概要」をご参照ください。

Q:音声が再生できないのはなぜですか?

以下のシナリオを一つずつ確認してください。

  1. 音声が完全なファイル(例:xx.mp3)として保存されている場合。

    1. 音声フォーマットの一貫性:リクエストパラメーターで設定された音声フォーマットがファイル拡張子と一致していることを確認してください。たとえば、WAV フォーマットを設定しているのに .mp3 拡張子で保存すると、再生が失敗します。

    2. プレーヤーの互換性:ご利用のプレーヤーが音声ファイルのフォーマットとサンプルレートをサポートしていることを確認してください。一部のプレーヤーは、高いサンプルレートや特定の音声エンコーディングをサポートしていない場合があります。

  2. 音声がストリームで再生される場合。

    1. 音声ストリームを完全なファイルとして保存し、プレーヤーで再生を試みてください。ファイルが再生できない場合は、シナリオ 1 のトラブルシューティング方法を参照してください。

    2. ファイルが正常に再生される場合、問題はストリーミング再生の実装にある可能性があります。ご利用のプレーヤーがストリーミング再生をサポートしていることを確認してください。

      ストリーミング再生をサポートする一般的なツールとライブラリには、FFmpeg、PyAudio(Python)、AudioFormat(Java)、MediaSource(JavaScript)などがあります。

Q:音声再生が途切れるのはなぜですか?

以下のシナリオを一つずつ確認してください。

  1. テキスト送信速度の確認:テキストセグメント間の間隔が適切であることを確認してください。前の音声セグメントの再生が終了した後、次のセグメントがすぐに送信されない状況を避けてください。

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

    • コールバック関数に過剰なビジネスロジックがあり、ブロッキングを引き起こしていないか確認してください。

    • コールバック関数は WebSocket スレッドで実行されます。ブロッキングが発生すると、WebSocket がネットワークパケットを時間内に受信できず、音声再生が途切れる可能性があります。

    • WebSocket スレッドのブロッキングを避けるため、音声データを別のバッファーに書き込み、別のスレッドで処理することを推奨します。

  3. ネットワーク安定性の確認:ネットワーク接続が安定していることを確認し、ネットワークの変動による音声送信の中断や遅延を避けてください。

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

以下の手順でトラブルシューティングしてください。

  1. 入力間隔の確認

    入力間隔を確認してください。ストリーミング音声合成を使用している場合、テキストセグメント間の送信間隔が長すぎないか(たとえば、数秒の遅延)確認してください。間隔が長いと、合計合成時間が増加します。

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

    • 最初のパケットのレイテンシー:通常約 500 ミリ秒。

    • RTF(RTF = 合計合成時間 / 音声持続時間):通常 1.0 未満。

Q:合成された音声の発音の誤りを処理するにはどうすればよいですか?

SSML の <phoneme> タグ を使用して、正しい発音を指定してください。

Q:音声が返されないのはなぜですか? 音声の末尾のテキストの一部が欠落しているのはなぜですか?(音声の欠落)

finish-task 命令 の送信を忘れていないか確認してください。合成中、サーバーは十分なテキストをキャッシュするまで待機してから開始します。finish-task の送信を忘れると、キャッシュ内の末尾のテキストが音声に変換されない可能性があります。

Q:音声ストリームの順序が乱れ、再生が文字化けするのはなぜですか?

この問題を以下の 2 つの領域でトラブルシューティングしてください。

  • 1 つの合成タスクに対する run-taskcontinue-task、および finish-task 命令 がすべて同じ task_id を使用していることを確認してください。

  • 非同期操作によって、バイナリデータが受信された順序と異なる順序で音声ファイルが書き込まれていないか確認してください。

Q:WebSocket 接続エラーを処理するにはどうすればよいですか?

  • WebSocket 接続のクローズ(コード 1007)を処理するにはどうすればよいですか?

    run-task 命令を送信した後、WebSocket 接続がコード 1007 で即座に閉じられます。

    • 根本原因: サーバーがプロトコルまたはデータフォーマットのエラーを検出し、切断します。一般的な理由は以下のとおりです。

      • run-task の payload に無効なフィールドがある場合(例:"input": {} 以外のフィールドを追加した場合)。

      • JSON フォーマットエラー(例:カンマの欠落や括弧の不一致)。

      • 必須フィールド(task_id や action など)の欠落。

    • 解決策:

      1. JSON フォーマットの検証:リクエストボディの構文を確認してください。

      2. 必須フィールドの確認:header.action、header.task_id、header.streaming、payload.task_group、payload.task、payload.function、payload.model、および payload.input がすべて設定されていることを確認してください。

      3. 無効なフィールドの削除:run-task の payload.input では、空のオブジェクト {} またはテキストフィールドのみを許可してください。他のフィールドを追加しないでください。

  • WebSocketBadStatus、401 Unauthorized、または 403 Forbidden エラーを処理するにはどうすればよいですか?

    WebSocket 接続が WebSocketBadStatus、401 Unauthorized、または 403 Forbidden で失敗します。

    • 根本原因: 認証失敗。サーバーは WebSocket ハンドシェイク中に Authorization ヘッダーを検証します。無効な API キーまたは欠落している API キーは拒否されます。

    • 解決策: 詳細については、「認証失敗のトラブルシューティング」をご参照ください。

権限および認証

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

ワークスペースを作成し、特定のモデルにのみ権限を付与することで、API キーのスコープを制限できます。詳細については、「ワークスペースの管理」をご参照ください。

その他の質問

GitHub の QA をご参照ください。