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

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

最終更新日:Dec 20, 2025
重要

中国 (北京) リージョンでモデルを使用するには、[API キー] ページ (中国 (北京) リージョン用) に移動します

このドキュメントでは、WebSocket 接続を使用して CosyVoice 音声合成サービスにアクセスする方法について説明します。

DashScope SDK は現在、Java と Python のみをサポートしています。他のプログラミング言語で CosyVoice 音声合成アプリケーションを開発したい場合は、WebSocket 接続を使用してサービスと通信する必要があります。

ユーザーガイド:モデルの詳細とモデル選択のガイダンスについては、「リアルタイム音声合成 - CosyVoice」をご参照ください。

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

一般的なプログラミング言語向けに、すぐに使用できる多くの WebSocket ライブラリとサンプルが利用可能です。例:

  • Go: gorilla/websocket

  • PHP: Ratchet

  • Node.js: ws

開発を開始する前に、WebSocket の基本原則と技術的な詳細を理解しておく必要があります。

前提条件

Model Studio を有効化し、API キーを作成済みであること。セキュリティリスクを防ぐため、API キーをコードにハードコーディングするのではなく、環境変数としてエクスポートしてください。

説明

サードパーティのアプリケーションやユーザーに一時的なアクセス権限を付与する場合、または機密データへのアクセスや削除などのリスクの高い操作を厳密に制御したい場合は、一時的な認証トークンを使用することを推奨します。

長期間有効な API キーと比較して、一時的な認証トークンは有効期間が短い (60 秒) ため、より安全です。一時的な呼び出しシナリオに適しており、API キー漏洩のリスクを効果的に低減できます。

一時的なトークンを使用するには、コード内の認証に使用する API キーを一時的な認証トークンに置き換えます。

モデルと価格

モデル

価格

cosyvoice-v3-plus

$0.286706/10,000 文字

cosyvoice-v3-flash

$0.14335/10,000 文字

cosyvoice-v2

$0.286706/10,000 文字

テキストとフォーマットの制限

テキスト長の制限

1 回の continue-task 命令で送信されるテキストは 2,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-v2cosyvoice-v3-flash、および cosyvoice-v3-plus モデルでのみ利用可能です。この機能は、基本的な算術、代数、幾何学など、小中学校で習う一般的な数式をサポートしています。

詳細については、「LaTeX 数式を音声に変換」をご参照ください。

SSML サポート

音声合成マークアップ言語 (SSML) 機能は現在、cosyvoice-v3-flash、cosyvoice-v3-plus、および cosyvoice-v2 モデルのクローン音声、および 音声リストでサポートされていると示されているシステム音声でのみ利用可能です。以下の条件を満たす必要があります:

この機能を使用するには:

  1. run-task 命令を送信する際に、enable_ssml パラメーターを true に設定して SSML サポートを有効にします。

  2. 次に、continue-task 命令を使用して SSML を含むテキストを送信します。

重要

enable_ssml パラメーターを true に設定して SSML サポートを有効にした後、合成する完全なテキストを 1 回の continue-task 命令で送信する必要があります。複数回にわたる送信はサポートされていません。

インタラクションフロー

image

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

以下は、クライアントとサーバー間のインタラクションフローです:

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

  2. タスクの開始:

    • クライアントは run-task 命令を送信してタスクを開始します。

    • クライアントはサーバーから task-started イベントを受信します。このイベントは、タスクが正常に開始されたことを示し、次のステップに進むことができます。

  3. 合成するテキストの送信:

    クライアントは、合成するテキストを含む 1 つ以上の continue-task 命令をサーバーに順次送信します。サーバーが完全な文を受信すると、音声ストリームを返します。テキスト長には制限があります。詳細については、continue-task 命令text フィールドの説明をご参照ください。

    説明

    複数の continue-task 命令を送信して、テキストの断片を順次送信できます。サーバーは断片を受信した後、自動的にテキストをセグメント化します:

    • 完全な文はすぐに合成され、クライアントはサーバーから対応する音声を受信します。

    • 不完全な文は、完全になるまでキャッシュされてから合成されます。サーバーは不完全な文に対して音声を返しません。

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

  4. サーバーにタスクの終了を通知:

    すべてのテキストが送信された後、クライアントは finish-task 命令をサーバーに送信してタスクを終了します。クライアントはサーバーから音声ストリームを受信し続けます。このステップを実行する必要があります。そうしないと、完全な音声ストリームを受信できない場合があります。

  5. タスクの終了:

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

  6. 接続のクローズ:クライアントは WebSocket 接続を閉じます。

URL

WebSocket の URL は固定です:

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

ヘッダー

リクエストヘッダーに以下のパラメーターを含めてください:

{
    "Authorization": "bearer <your_dashscope_api_key>", // 必須。 <your_dashscope_api_key> をご自身の API キーに置き換えてください。
    "user-agent": "your_platform_info", // 任意。
    "X-DashScope-WorkSpace": workspace, // 任意。ご利用の Model Studio ワークスペースの ID。
    "X-DashScope-DataInspection": "enable"
}

命令 (クライアントからサーバーへ)

命令は、クライアントがサーバーにテキストフレームとして JSON 形式で送信するメッセージです。これらの命令は、タスクの開始と終了を制御し、タスクの境界を識別します。

タスクの失敗を避けるため、命令は以下の順序で送信する必要があります:

  1. run-task 命令の送信

  2. continue-task 命令の送信

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

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

  3. finish-task 命令の送信

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

    • この命令は、すべての continue-task 命令が送信された後に送信してください。

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

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

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

  • 合成するテキストを送信しない:この命令に合成するテキストを含めないでください。テキストを含めると、トラブルシューティングが困難になる可能性があります。

例:

{
    "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 です。ハイフンでフォーマットすることも (例:"2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx")、ハイフンなしでフォーマットすることもできます (例:"2bf83b9abaeb4fda8d9axxxxxxxxxxxx")。ほとんどのプログラミング言語には、UUID を生成するための組み込み API があります。例えば、Python では:

import uuid

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

後続の continue-task 命令および finish-task 命令で使用される task_id は、run-task 命令で使用されたものと同じでなければなりません。

header.streaming

string

はい

固定文字列:"duplex"

payload パラメーター

パラメーター

タイプ

必須

説明

payload.task_group

string

はい

固定文字列:"audio"。

payload.task

string

はい

固定文字列:"tts"。

payload.function

string

はい

固定文字列:"SpeechSynthesizer"。

payload.model

string

はい

音声合成モデル

異なるモデルには対応する音声が必要です:

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

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

  • 完全なリストについては、「音声リスト」をご参照ください。

payload.input

object

はい

  • この時点で合成するテキストを送信しない場合、input のフォーマットは次のようになります:

    "input": {}
  • この時点で合成するテキストを送信する場合、input のフォーマットは次のようになります:

    "input": {
      "text": "今日の天気はどうですか?" // 合成するテキスト
    }

payload.parameters

text_type

string

はい

固定文字列:"PlainText"。

voice

string

はい

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

システム音声とクローン音声がサポートされています:

  • システム音声:「音声リスト」をご参照ください。

  • クローン音声音声クローン機能を使用して音声をカスタマイズします。クローン音声を使用する場合、音声クローンと音声合成の両方に同じアカウントが使用されていることを確認してください。詳細な手順については、「CosyVoice 音声クローン API」をご参照ください。

    クローン音声を使用する場合、リクエストの model パラメーターの値は、音声の作成に使用されたモデルバージョン (target_model パラメーター) と同じでなければなりません。

format

string

いいえ

音声コーディング形式。

サポートされているフォーマットは pcm、wav、mp3 (デフォルト)、opus です。

音声フォーマットが 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 に設定されている場合、テキストは 1 回しか送信できません (continue-task 命令は 1 回しか送信できません)。

bit_rate

int

いいえ

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

デフォルト値:32。

値の範囲:[6, 510]。

word_timestamp_enabled

boolean

いいえ

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

デフォルト値:false。

  • true

  • false

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

seed

int

いいえ

生成中に使用される乱数シードで、合成効果を変化させます。モデルのバージョン、テキスト、音声、その他のパラメーターが同じ場合、同じシードを使用すると同じ合成結果が再現されます。

デフォルト値:0。

値の範囲:[0, 65535]。

language_hints

array[string]

いいえ

言語のヒントを提供します。cosyvoice-v3-flash と cosyvoice-v3-plus のみがこの機能をサポートしています。

デフォルト値はありません。このパラメーターは設定されていない場合、効果はありません。

このパラメーターは、音声合成において以下の効果があります:

  1. テキスト正規化 (TN) 処理の言語を指定し、数字、略語、記号の読み方に影響を与えます。これは中国語と英語でのみ有効です。

    値の範囲:

    • zh:中国語

    • en:英語

  2. 音声合成のターゲット言語を指定して (クローン音声のみ)、合成精度を向上させます。これは英語、フランス語、ドイツ語、日本語、韓国語、ロシア語で有効です。中国語を指定する必要はありません。値は、音声クローン時に使用された languageHints/language_hints と一致している必要があります。

    値の範囲:

    • en:英語

    • fr:フランス語

    • de:ドイツ語

    • ja:日本語

    • ko:韓国語

    • ru:ロシア語

指定された言語ヒントがテキストの内容と明らかに一致しない場合、例えば中国語のテキストに en を設定した場合、ヒントは無視され、言語はテキストの内容に基づいて自動的に検出されます。

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

instruction

string

いいえ

命令を設定します。この機能は、cosyvoice-v3-flash および cosyvoice-v3-plus モデルのクローン音声、および 音声リストでサポートされているとマークされたシステム音声でのみ利用可能です。プロンプトは中国語で、特定のパターンに従う必要があります。

デフォルト値はありません。このパラメーターは設定されていない場合、効果はありません。

この命令は、音声合成において以下の効果があります:

  1. 非中国語を指定する (クローン音声のみ)

    • フォーマット:「你会用<小语种>说出来。」(注:中国語を使用し、文末の「」を省略しないでください。「<小语种>」を特定の言語、例えば 德语 に置き換えます。)

    • 例:「你会用德语说出来。

    • サポートされている言語:法语 (フランス語)、德语 (ドイツ語)、日语 (日本語)、韩语 (韓国語)、俄语 (ロシア語)。

  2. 方言を指定する (クローン音声のみ)

    • フォーマット:「请用<方言>表达。」(注:中国語を使用し、文末の「」を省略しないでください。「<方言>」を特定の方言、例えば 广东话 に置き換えます。)

    • 例:「请用广东话表达。

    • サポートされている方言:广东话 (広東語)、东北话 (東北方言)、甘肃话 (甘粛方言)、贵州话 (貴州方言)、河南话 (河南方言)、湖北话 (湖北方言)、江西话 (江西方言)、闽南话 (閩南語)、宁夏话 (寧夏方言)、山西话 (山西方言)、陕西话 (陝西)、山东话 (山東方言)、上海话 (上海語)、四川话 (四川方言)、天津话 (天津方言)、云南话 (雲南)。

  3. 感情、シナリオ、役割、またはアイデンティティを指定します。一部のシステム音声のみがこの機能をサポートしており、音声によって異なります。詳細については、「音声リスト」をご参照ください。

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 モデルでのみ利用可能です。

2. continue-task 命令

この命令は、合成するテキストを送信するために使用されます。

合成するテキストを 1 回の continue-task 命令で一度に送信することも、テキストを分割して複数の continue-task 命令で順次送信することもできます。

重要

送信タイミング:task-started イベントを受信した後にこの命令を送信してください。

説明

テキストの断片を送信する間隔は 23 秒を超えてはなりません。この間隔を超えると、「request timeout after 23 seconds」例外がトリガーされます。

送信するテキストがない場合は、速やかに finish-task 命令を送信してタスクを終了してください。

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

例:

{
    "header": {
        "action": "continue-task",
        "task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx", // ランダムな UUID
        "streaming": "duplex"
    },
    "payload": {
        "input": {
            "text": "牀前月光を看る、疑ふらくは是れ地上の霜かと。頭を挙げて山月を望み、頭を低れて故郷を思ふ。"
        }
    }
}

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 命令:タスクの終了

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

合成された音声の一部が欠落しないように、この命令を送信する必要があります。

クライアントがこの命令を送信すると、サーバーは残りのテキストを音声に変換します。合成が完了すると、サーバーはクライアントに task-finished イベントを返します。

重要

送信タイミング:すべての 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 命令を送信する前に、このイベントを受信する必要があります。そうしないと、タスクは失敗します。

payloadtask-started イベントは空です。

例:

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

header パラメーター

パラメーター

タイプ

説明

header.event

string

イベントのタイプ。

このイベントでは、値は "task-started" に固定されます。

header.task_id

string

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

2. result-generated イベント

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

CosyVoice サービスでは、result-generated イベントはプロトコルのために予約されています。リクエスト ID などの情報をカプセル化しており、無視できます。

例:

{
    "header": {
        "task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx",
        "event": "result-generated",
        "attributes": {
            "request_uuid": "0a9dba9e-d3a6-45a4-be6d-xxxxxxxxxxxx"
        }
    },
    "payload": {}
}

header パラメーター

パラメーター

タイプ

説明

header.event

string

イベントのタイプ。

このイベントでは、値は "result-generated" に固定されます。

header.task_id

string

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

header.attributes.request_uuid

string

リクエスト ID。

payload パラメーターの説明

パラメーター

タイプ

説明

payload.usage.characters

integer

現在のリクエストでこれまでに課金対象となった文字数。 1 つのタスク内で、usageresult-generated イベントまたは task-finished イベントに現れることがあります。返される usage フィールドは累積値です。最終的な値を標準として使用してください。

3. task-finished イベント:タスクが終了しました

サーバーから task-finished イベントを受信した場合、これはタスクが完了したことを示します。

タスクが終了した後、WebSocket 接続を閉じてプログラムを終了できます。また、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

string

イベントのタイプ。

このイベントでは、値は "task-finished" に固定されます。

header.task_id

string

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

header.attributes.request_uuid

string

リクエスト ID。これを CosyVoice の開発者に提供して、問題を特定できます。

payload パラメーター

パラメーター

タイプ

説明

payload.usage.characters

integer

現在のリクエストでこれまでに課金対象となった文字数。 1 つのタスク内で、usageresult-generated イベントまたは task-finished イベントに現れることがあります。返される usage フィールドは累積値です。最終的な値を標準として使用してください。

payload.output.sentence.index

integer

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

これらのフィールドは、word_timestamp_enabled を true に設定して文字レベルのタイムスタンプが有効になっている場合にのみ返されます。

payload.output.sentence.words[k]

text

string

文字のテキスト。

begin_index

integer

文中の文字の開始位置インデックス。0 から始まります。

end_index

integer

文中の文字の終了位置インデックス。1 から始まります。

begin_time

integer

文字に対応する音声の開始タイムスタンプ (ミリ秒)。

end_time

integer

文字に対応する音声の終了タイムスタンプ (ミリ秒)。

word_timestamp_enabled を使用して文字レベルのタイムスタンプを有効にすると、応答にタイムスタンプ情報が含まれます。以下に例を示します:

クリックして文字レベルのタイムスタンプを有効にした後の応答を表示

{
    "header": {
        "task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx",
        "event": "task-finished",
        "attributes": {"request_uuid": "0a9dba9e-d3a6-45a4-be6d-xxxxxxxxxxxx"}
    },
    "payload": {
        "output": {
            "sentence": {
                "index": 0,
                "words": [
                    {
                        "text": "What",
                        "begin_index": 0,
                        "end_index": 1,
                        "begin_time": 80,
                        "end_time": 200
                    },
                    {
                        "text": "is",
                        "begin_index": 1,
                        "end_index": 2,
                        "begin_time": 240,
                        "end_time": 360
                    },
                    {
                        "text": "the",
                        "begin_index": 2,
                        "end_index": 3,
                        "begin_time": 360,
                        "end_time": 480
                    },
                    {
                        "text": "weather",
                        "begin_index": 3,
                        "end_index": 4,
                        "begin_time": 480,
                        "end_time": 680
                    },
                    {
                        "text": "like",
                        "begin_index": 4,
                        "end_index": 5,
                        "begin_time": 680,
                        "end_time": 800
                    },
                    {
                        "text": "to",
                        "begin_index": 5,
                        "end_index": 6,
                        "begin_time": 800,
                        "end_time": 920
                    },
                    {
                        "text": "day",
                        "begin_index": 6,
                        "end_index": 7,
                        "begin_time": 920,
                        "end_time": 1000
                    },
                    {
                        "text": "?",
                        "begin_index": 7,
                        "end_index": 8,
                        "begin_time": 1000,
                        "end_time": 1320
                    }
                ]
            }
        },
        "usage": {"characters": 15}
    }
}

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

string

イベントのタイプ。

このイベントでは、値は "task-failed" に固定されます。

header.task_id

string

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

header.error_code

string

エラータイプの記述。

header.error_message

string

エラーの具体的な原因。

接続のオーバーヘッドと再利用

WebSocket サービスは接続の再利用をサポートしています。これにより、リソースの利用率が向上し、新しい接続を確立する際のオーバーヘッドが削減されます。

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

重要
  1. 再利用された接続内の各タスクには、一意の task_id が必要です。

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

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

サンプルコード

サンプルコードは、サービスを実行するための基本的な実装を提供します。特定のビジネスシナリオに合わせてコードを開発する必要があります。

WebSocket クライアントコードを作成する場合、通常、メッセージを同時に送受信するために非同期プログラミングが使用されます。以下の手順に従ってプログラムを作成できます:

  1. WebSocket 接続の確立

    WebSocket ライブラリ関数を呼び出し、ヘッダーURL を渡して WebSocket 接続を確立できます。具体的な実装は、プログラミング言語やライブラリによって異なります。

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

    WebSocket ライブラリが提供するコールバック関数 (オブザーバーパターン) を使用して、サーバーから返されるメッセージをリッスンできます。具体的な実装は、プログラミング言語によって異なります。

    サーバーは、バイナリ音声ストリームとイベントの 2 種類のメッセージを返します。

    イベントのリッスン

    • task-started:task-started イベントを受信すると、タスクが正常に開始されたことを示します。このイベントがトリガーされた後にのみ、サーバーに continue-task 命令または finish-task 命令を送信できます。そうしないと、タスクは失敗します。

    • result-generated: クライアントが continue-task 命令または finish-task 命令を送信すると、サーバーは result-generated イベントを繰り返し返すことがあります。このイベントは CosyVoice プロトコルの予約済みの部分であり、無視しても問題ありません。

    • task-finished:task-finished イベントを受信すると、タスクが完了したことを示します。その後、WebSocket 接続を閉じてプログラムを終了できます。

    • task-failed:task-failed イベントを受信した場合、タスクは失敗しています。WebSocket 接続を閉じ、エラーメッセージに基づいてコードを修正して問題を解決してください。

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

    • ストリーミング音声合成では、MP3 や Opus などの圧縮フォーマットの場合、音声セグメントを再生するためにストリーミングプレーヤーを使用します。デコードの失敗を避けるため、フレームごとに再生しないでください。

      ストリーミング再生をサポートするプレーヤーには、ffmpeg、pyaudio (Python)、AudioFormat (Java)、MediaSource (JavaScript) などがあります。
    • 音声データを完全な音声ファイルに結合する場合は、同じファイルにデータを追加します。

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

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

    サーバーメッセージをリッスンしているスレッドとは別のスレッド、例えばメインスレッドでサーバーに命令を送信できます。具体的な実装は、プログラミング言語によって異なります。

    タスクの失敗を避けるため、命令は以下の順序で送信する必要があります:

    1. run-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 (
	wsURL      = "wss://dashscope.aliyuncs.com/api-ws/v1/inference/"
	outputFile = "output.mp3"
)

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

	// 出力ファイルをクリア
	os.Remove(outputFile)
	os.Create(outputFile)

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

	conn, resp, err := websocket.DefaultDialer.Dial(wsURL, header)
	if err != nil {
		if resp != nil {
			fmt.Printf("接続に失敗しました。HTTP ステータスコード: %d\n", resp.StatusCode)
		}
		fmt.Println("接続に失敗しました:", err)
		return
	}
	defer conn.Close()

	// タスク ID を生成
	taskID := uuid.New().String()
	fmt.Printf("生成されたタスク ID: %s\n", taskID)

	// run-task 命令を送信
	runTaskCmd := map[string]interface{}{
		"header": map[string]interface{}{
			"action":    "run-task",
			"task_id":   taskID,
			"streaming": "duplex",
		},
		"payload": map[string]interface{}{
			"task_group": "audio",
			"task":       "tts",
			"function":   "SpeechSynthesizer",
			"model":      "cosyvoice-v3-flash",
			"parameters": map[string]interface{}{
				"text_type":   "PlainText",
				"voice":       "longanyang",
				"format":      "mp3",
				"sample_rate": 22050,
				"volume":      50,
				"rate":        1,
				"pitch":       1,
				// enable_ssml が true に設定されている場合、continue-task 命令は 1 回しか送信できません。そうしないと、「Text request limit violated, expected 1.」というエラーが報告されます。
				"enable_ssml": false,
			},
			"input": map[string]interface{}{},
		},
	}

	runTaskJSON, _ := json.Marshal(runTaskCmd)
	fmt.Printf("run-task 命令を送信中: %s\n", string(runTaskJSON))

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

	textSent := false

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

		// バイナリメッセージを処理
		if messageType == websocket.BinaryMessage {
			fmt.Printf("バイナリメッセージを受信しました、長さ: %d\n", len(message))
			file, _ := os.OpenFile(outputFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
			file.Write(message)
			file.Close()
			continue
		}

		// テキストメッセージを処理
		messageStr := string(message)
		fmt.Printf("テキストメッセージを受信しました: %s\n", strings.ReplaceAll(messageStr, "\n", ""))

		// イベントタイプを取得するための簡単な JSON 解析
		var msgMap map[string]interface{}
		if json.Unmarshal(message, &msgMap) == nil {
			if header, ok := msgMap["header"].(map[string]interface{}); ok {
				if event, ok := header["event"].(string); ok {
					fmt.Printf("イベントタイプ: %s\n", event)

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

						if !textSent {
							// continue-task 命令を送信

							texts := []string{"牀前月光を看る、疑ふらくは是れ地上の霜かと。", "頭を挙げて山月を望み、頭を低れて故郷を思ふ。"}

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

								continueTaskJSON, _ := json.Marshal(continueTaskCmd)
								fmt.Printf("continue-task 命令を送信中: %s\n", string(continueTaskJSON))

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

							textSent = true

							// finish-task の送信を遅延
							time.Sleep(500 * time.Millisecond)

							// finish-task 命令を送信
							finishTaskCmd := map[string]interface{}{
								"header": map[string]interface{}{
									"action":    "finish-task",
									"task_id":   taskID,
									"streaming": "duplex",
								},
								"payload": map[string]interface{}{
									"input": map[string]interface{}{},
								},
							}

							finishTaskJSON, _ := json.Marshal(finishTaskCmd)
							fmt.Printf("finish-task 命令を送信中: %s\n", string(finishTaskJSON))

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

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

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

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

C#

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

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

    // WebSocket サーバーエンドポイント
    private const string WebSocketUrl = "wss://dashscope.aliyuncs.com/api-ws/v1/inference/";
    // 出力ファイルパス
    private const string OutputFilePath = "output.mp3";

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

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

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

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

            // run-task 命令を送信
            _taskId = GenerateTaskId();
            await SendRunTaskCommandAsync(_taskId);

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

            // continue-task 命令を継続的に送信
            string[] texts = {
                "牀前月光を看る",
                "疑ふらくは是れ地上の霜かと",
                "頭を挙げて山月を望み",
                "頭を低れて故郷を思ふ"
            };
            foreach (string text in texts) {
                await SendContinueTaskCommandAsync(text);
            }

            // finish-task 命令を送信
            await SendFinishTaskCommandAsync(_taskId);

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

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

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

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

        // WebSocket 接続のヘッダーを設定
        _webSocket.Options.SetRequestHeader("Authorization", $"bearer {ApiKey}");
        _webSocket.Options.SetRequestHeader("X-DashScope-DataInspection", "enable");

        try {
            await _webSocket.ConnectAsync(uri, _cancellationTokenSource.Token);
            Console.WriteLine("WebSocket サービスに正常に接続しました。");
        } catch (OperationCanceledException) {
            Console.WriteLine("WebSocket 接続はキャンセルされました。");
        } catch (Exception ex) {
            Console.WriteLine($"WebSocket 接続に失敗しました: {ex.Message}");
            throw;
        }
    }

    private static async Task SendRunTaskCommandAsync(string taskId) {
        var command = CreateCommand("run-task", taskId, "duplex", new {
            task_group = "audio",
            task = "tts",
            function = "SpeechSynthesizer",
            model = "cosyvoice-v3-flash",
            parameters = new
            {
                text_type = "PlainText",
                voice = "longanyang",
                format = "mp3",
                sample_rate = 22050,
                volume = 50,
                rate = 1,
                pitch = 1,
                // enable_ssml が true に設定されている場合、continue-task 命令は 1 回しか送信できません。そうしないと、「Text request limit violated, expected 1.」というエラーが報告されます。
                enable_ssml = false
            },
            input = new { }
        });

        await SendJsonMessageAsync(command);
        Console.WriteLine("run-task 命令が送信されました。");
    }

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

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

        await SendJsonMessageAsync(command);
        Console.WriteLine("continue-task 命令が送信されました。");
    }

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

        await SendJsonMessageAsync(command);
        Console.WriteLine("finish-task 命令が送信されました。");
    }

    private static async Task SendJsonMessageAsync(string message) {
        var buffer = Encoding.UTF8.GetBytes(message);
        try {
            await _webSocket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, _cancellationTokenSource.Token);
        } catch (OperationCanceledException) {
            Console.WriteLine("メッセージの送信はキャンセルされました。");
        }
    }

    private static async Task ReceiveMessagesAsync() {
        while (_webSocket.State == WebSocketState.Open) {
            var response = await ReceiveMessageAsync();
            if (response != null) {
                var eventStr = response.RootElement.GetProperty("header").GetProperty("event").GetString();
                switch (eventStr) {
                    case "task-started":
                        Console.WriteLine("タスクが開始されました。");
                        _taskStartedTcs.TrySetResult(true);
                        break;
                    case "task-finished":
                        Console.WriteLine("タスクが終了しました。");
                        _cancellationTokenSource.Cancel();
                        break;
                    case "task-failed":
                        Console.WriteLine("タスクが失敗しました: " + response.RootElement.GetProperty("header").GetProperty("error_message").GetString());
                        _cancellationTokenSource.Cancel();
                        break;
                    default:
                        // result-generated はここで処理できます
                        break;
                }
            }
        }
    }

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

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

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

            if (result.MessageType == WebSocketMessageType.Binary) {
                // バイナリデータを処理
                Console.WriteLine("バイナリデータを受信中...");

                // バイナリデータをファイルに保存
                using (var fileStream = new FileStream(OutputFilePath, FileMode.Append)) {
                    fileStream.Write(buffer, 0, result.Count);
                }

                return null;
            }

            string message = Encoding.UTF8.GetString(buffer, 0, result.Count);
            return JsonDocument.Parse(message);
        } catch (OperationCanceledException) {
            Console.WriteLine("メッセージの受信はキャンセルされました。");
            return null;
        }
    }

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

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

        return JsonSerializer.Serialize(command);
    }
}

PHP

サンプルコードのディレクトリ構造は次のとおりです:

my-php-project/

├── composer.json

├── vendor/

└── index.php

以下は composer.json の内容です。必要に応じて依存関係のバージョン番号を指定できます:

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

index.php の内容は次のとおりです:

<?php

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

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

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

$loop = Loop::get();

if (file_exists($output_file)) {
    // ファイルの内容をクリア
    file_put_contents($output_file, '');
}

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

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

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

$connector($websocket_url, [], $headers)->then(function ($conn) use ($loop, $output_file) {
    echo "WebSocket サーバーに接続しました\n";

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

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

    // continue-task 命令を送信する関数を定義
    $sendContinueTask = function() use ($conn, $loop, $taskId) {
        // 送信するテキスト
        $texts = ["牀前月光を看る", "疑ふらくは是れ地上の霜かと", "頭を挙げて山月を望み", "頭を低れて故郷を思ふ"];
        $continueTaskCount = 0;
        foreach ($texts as $text) {
            $continueTaskMessage = json_encode([
                "header" => [
                    "action" => "continue-task",
                    "task_id" => $taskId,
                    "streaming" => "duplex"
                ],
                "payload" => [
                    "input" => [
                        "text" => $text
                    ]
                ]
            ]);
            echo "continue-task 命令の送信準備中: " . $continueTaskMessage . "\n";
            $conn->send($continueTaskMessage);
            $continueTaskCount++;
        }
        echo "送信された continue-task 命令の数: " . $continueTaskCount . "\n";

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

    // task-started イベントが受信されたかどうかを確認するフラグ
    $taskStarted = false;

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

            if (isset($response['header']['event'])) {
                handleEvent($conn, $response, $sendContinueTask, $loop, $taskId, $taskStarted);
            } else {
                echo "不明なメッセージ形式\n";
            }
        }
    });

    // 接続クローズをリッスン
    $conn->on('close', function($code = null, $reason = null) {
        echo "接続が閉じられました\n";
        if ($code !== null) {
            echo "クローズコード: " . $code . "\n";
        }
        if ($reason !== null) {
            echo "クローズ理由: " . $reason . "\n";
        }
    });
}, function ($e) {
    echo "接続できませんでした: {$e->getMessage()}\n";
});

$loop->run();

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

/**
 * run-task 命令を送信
 * @param $conn
 * @param $taskId
 */
function sendRunTaskMessage($conn, $taskId) {
    $runTaskMessage = json_encode([
        "header" => [
            "action" => "run-task",
            "task_id" => $taskId,
            "streaming" => "duplex"
        ],
        "payload" => [
            "task_group" => "audio",
            "task" => "tts",
            "function" => "SpeechSynthesizer",
            "model" => "cosyvoice-v3-flash",
            "parameters" => [
                "text_type" => "PlainText",
                "voice" => "longanyang",
                "format" => "mp3",
                "sample_rate" => 22050,
                "volume" => 50,
                "rate" => 1,
                "pitch" => 1,
                // enable_ssml が true に設定されている場合、continue-task 命令は 1 回しか送信できません。そうしないと、「Text request limit violated, expected 1.」というエラーが報告されます。
                "enable_ssml" => false
            ],
            "input" => (object) []
        ]
    ]);
    echo "run-task 命令の送信準備中: " . $runTaskMessage . "\n";
    $conn->send($runTaskMessage);
    echo "run-task 命令が送信されました\n";
}

/**
 * 音声ファイルを読み込む
 * @param string $filePath
 * @return bool|string
 */
function readAudioFile(string $filePath) {
    $voiceData = file_get_contents($filePath);
    if ($voiceData === false) {
        echo "音声ファイルを読み込めませんでした\n";
    }
    return $voiceData;
}

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

/**
 * finish-task 命令を送信
 * @param $conn
 * @param $taskId
 */
function sendFinishTaskMessage($conn, $taskId) {
    $finishTaskMessage = json_encode([
        "header" => [
            "action" => "finish-task",
            "task_id" => $taskId,
            "streaming" => "duplex"
        ],
        "payload" => [
            "input" => (object) []
        ]
    ]);
    echo "finish-task 命令の送信準備中: " . $finishTaskMessage . "\n";
    $conn->send($finishTaskMessage);
    echo "finish-task 命令が送信されました\n";
}

/**
 * イベントを処理
 * @param $conn
 * @param $response
 * @param $sendContinueTask
 * @param $loop
 * @param $taskId
 * @param $taskStarted
 */
function handleEvent($conn, $response, $sendContinueTask, $loop, $taskId, &$taskStarted) {
    switch ($response['header']['event']) {
        case 'task-started':
            echo "タスクが開始されました、continue-task 命令を送信中...\n";
            $taskStarted = true;
            // continue-task 命令を送信
            $sendContinueTask();
            break;
        case 'result-generated':
            // result-generated イベントを無視
            break;
        case 'task-finished':
            echo "タスクが終了しました\n";
            $conn->close();
            break;
        case 'task-failed':
            echo "タスクが失敗しました\n";
            echo "エラーコード: " . $response['header']['error_code'] . "\n";
            echo "エラーメッセージ: " . $response['header']['error_message'] . "\n";
            $conn->close();
            break;
        case 'error':
            echo "エラー: " . $response['payload']['message'] . "\n";
            break;
        default:
            echo "不明なイベント: " . $response['header']['event'] . "\n";
            break;
    }

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

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

Node.js

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

npm install ws
npm install uuid

サンプルコードは次のとおりです:

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

// API キーを環境変数として設定していない場合は、次の行を apiKey = 'your_api_key' に置き換えることができます。本番環境では、漏洩のリスクを減らすために API キーをコードにハードコーディングすることは推奨されません。
const apiKey = process.env.DASHSCOPE_API_KEY;
// WebSocket サーバーエンドポイント
const url = 'wss://dashscope.aliyuncs.com/api-ws/v1/inference/';
// 出力ファイルパス
const outputFilePath = 'output.mp3';

// 出力ファイルをクリア
fs.writeFileSync(outputFilePath, '');

// WebSocket クライアントを作成
const ws = new WebSocket(url, {
  headers: {
    Authorization: `bearer ${apiKey}`,
    'X-DashScope-DataInspection': 'enable'
  }
});

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

ws.on('open', () => {
  console.log('WebSocket サーバーに接続しました');

  // run-task 命令を送信
  const runTaskMessage = JSON.stringify({
    header: {
      action: 'run-task',
      task_id: taskId,
      streaming: 'duplex'
    },
    payload: {
      task_group: 'audio',
      task: 'tts',
      function: 'SpeechSynthesizer',
      model: 'cosyvoice-v3-flash',
      parameters: {
        text_type: 'PlainText',
        voice: 'longanyang', // 音声
        format: 'mp3', // 音声フォーマット
        sample_rate: 22050, // サンプルレート
        volume: 50, // ボリューム
        rate: 1, // 話速
        pitch: 1, // ピッチ
        enable_ssml: false // SSML 機能を有効にするかどうかを指定します。enable_ssml が true に設定されている場合、continue-task 命令は 1 回しか送信できません。そうしないと、「Text request limit violated, expected 1.」というエラーが報告されます。
      },
      input: {}
    }
  });
  ws.send(runTaskMessage);
  console.log('run-task メッセージが送信されました');
});

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

    switch (message.header.event) {
      case 'task-started':
        taskStarted = true;
        console.log('タスクが開始されました');
        // continue-task 命令を送信
        sendContinueTasks(ws);
        break;
      case 'task-finished':
        console.log('タスクが終了しました');
        ws.close();
        fileStream.end(() => {
          console.log('ファイルストリームが閉じられました');
        });
        break;
      case 'task-failed':
        console.error('タスクが失敗しました: ', message.header.error_message);
        ws.close();
        fileStream.end(() => {
          console.log('ファイルストリームが閉じられました');
        });
        break;
      default:
        // ここで result-generated を処理できます
        break;
    }
  }
});

function sendContinueTasks(ws) {
  const texts = [
    '牀前月光を看る、',
    '疑ふらくは是れ地上の霜かと。',
    '頭を挙げて山月を望み、',
    '頭を低れて故郷を思ふ。'
  ];

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

  // finish-task 命令を送信
  setTimeout(() => {
    if (taskStarted) {
      const finishTaskMessage = JSON.stringify({
        header: {
          action: 'finish-task',
          task_id: taskId,
          streaming: 'duplex'
        },
        payload: {
          input: {}
        }
      });
      ws.send(finishTaskMessage);
      console.log('finish-task が送信されました');
    }
  }, texts.length * 1000 + 1000); // すべての continue-task 命令が送信された 1 秒後に送信
}

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

Java

Java プログラミング言語を使用する場合、Java DashScope SDK で開発することを推奨します。詳細については、「Java SDK」をご参照ください。

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

  • Java-WebSocket

  • jackson-databind

Maven または Gradle を使用して依存関係パッケージを管理することを推奨します。以下に構成を示します:

pom.xml

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

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

build.gradle

// 他のコードを省略
dependencies {
  // WebSocket クライアント
  implementation 'org.java-websocket:Java-WebSocket:1.5.3'
  // JSON 処理
  implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.0'
}
// 他のコードを省略

Java コードは次のとおりです:

import com.fasterxml.jackson.databind.ObjectMapper;

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

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

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

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

    @Override
    public void onOpen(ServerHandshake serverHandshake) {
        System.out.println("接続に成功しました");

        // run-task 命令を送信
        // enable_ssml が true に設定されている場合、continue-task 命令は 1 回しか送信できません。そうしないと、「Text request limit violated, expected 1.」というエラーが報告されます。
        String runTaskCommand = "{ \"header\": { \"action\": \"run-task\", \"task_id\": \"" + taskId + "\", \"streaming\": \"duplex\" }, \"payload\": { \"task_group\": \"audio\", \"task\": \"tts\", \"function\": \"SpeechSynthesizer\", \"model\": \"cosyvoice-v3-flash\", \"parameters\": { \"text_type\": \"PlainText\", \"voice\": \"longanyang\", \"format\": \"mp3\", \"sample_rate\": 22050, \"volume\": 50, \"rate\": 1, \"pitch\": 1, \"enable_ssml\": false }, \"input\": {} }}";
        send(runTaskCommand);
    }

    @Override
    public void onMessage(String message) {
        System.out.println("サーバーからメッセージを受信しました: " + message);
        try {
            // JSON メッセージを解析
            Map<String, Object> messageMap = new ObjectMapper().readValue(message, Map.class);

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

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

                    if ("task-started".equals(event)) {
                        System.out.println("サーバーから task-started イベントを受信しました");

                        List<String> texts = Arrays.asList(
                                "牀前月光を看る、疑ふらくは是れ地上の霜かと。",
                                "頭を挙げて山月を望み、頭を低れて故郷を思ふ。"
                        );

                        for (String text : texts) {
                            // continue-task 命令を送信
                            sendContinueTask(text);
                        }

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

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

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

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

    @Override
    public void onError(Exception ex) {
        System.err.println("エラーが発生しました: " + ex.getMessage());
        ex.printStackTrace();
    }

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

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

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

    public static void main(String[] args) {
        try {
            String apiKey = System.getenv("DASHSCOPE_API_KEY");
            if (apiKey == null || apiKey.isEmpty()) {
                System.err.println("DASHSCOPE_API_KEY 環境変数を設定してください");
                return;
            }

            Map<String, String> headers = new HashMap<>();
            headers.put("Authorization", "bearer " + apiKey);
            TTSWebSocketClient client = new TTSWebSocketClient(new URI("wss://dashscope.aliyuncs.com/api-ws/v1/inference/"), headers);

            client.connect();

            while (!client.isClosed() && !client.taskFinished) {
                Thread.sleep(1000);
            }
        } catch (Exception e) {
            System.err.println("WebSocket サービスへの接続に失敗しました: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

Python

Python プログラミング言語を使用する場合、Python DashScope SDK で開発することを推奨します。詳細については、「Python SDK」をご参照ください。

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

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

サンプルコードを実行する Python ファイルに "websocket.py" という名前を付けないでください。そうしないと、`AttributeError: module 'websocket' has no attribute 'WebSocketApp'. Did you mean: 'WebSocket'?` というエラーが報告されます。

import websocket
import json
import uuid
import os
import time


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

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

    def on_open(self, ws):
        """
    WebSocket 接続が確立されたときのコールバック関数。
    音声合成タスクを開始するために run-task 命令を送信します。
    """
        print("WebSocket が接続されました")

        # run-task 命令を構築
        run_task_cmd = {
            "header": {
                "action": "run-task",
                "task_id": self.task_id,
                "streaming": "duplex"
            },
            "payload": {
                "task_group": "audio",
                "task": "tts",
                "function": "SpeechSynthesizer",
                "model": "cosyvoice-v3-flash",
                "parameters": {
                    "text_type": "PlainText",
                    "voice": "longanyang",
                    "format": "mp3",
                    "sample_rate": 22050,
                    "volume": 50,
                    "rate": 1,
                    "pitch": 1,
                    # enable_ssml が True に設定されている場合、continue-task 命令は 1 回しか送信できません。そうしないと、「Text request limit violated, expected 1.」というエラーが報告されます。
                    "enable_ssml": False
                },
                "input": {}
            }
        }

        # run-task 命令を送信
        ws.send(json.dumps(run_task_cmd))
        print("run-task 命令が送信されました")

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

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

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

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

                            # continue-task 命令を送信
                            texts = [
                                "牀前月光を看る、疑ふらくは是れ地上の霜かと。",
                                "頭を挙げて山月を望み、頭を低れて故郷を思ふ。"
                            ]

                            for text in texts:
                                self.send_continue_task(text)

                            # すべての continue-task 命令が送信された後、finish-task を送信
                            self.send_finish_task()

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

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

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

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

    def on_close(self, ws, close_status_code, close_msg):
        """接続が閉じられたときのコールバック"""
        print(f"WebSocket が閉じられました: {close_msg} ({close_status_code})")

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

        self.ws.send(json.dumps(cmd))
        print(f"continue-task 命令が送信されました、テキスト内容: {text}")

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

        self.ws.send(json.dumps(cmd))
        print("finish-task 命令が送信されました")

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

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

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

        print("WebSocket メッセージをリッスン中...")
        self.ws.run_forever()  # 持続的な接続リスナーを開始


# 使用例
if __name__ == "__main__":
    API_KEY = os.environ.get("DASHSCOPE_API_KEY")  # API キーを環境変数として設定していない場合は、API_KEY をご自身の API キーに設定してください。
    SERVER_URI = "wss://dashscope.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 を使用すると、サーバーとクライアントの両方がデータを送信できます。これは、リアルタイムの音声合成や認識の進捗をプッシュするなどの機能に必要です。対照的に、RESTful API は HTTP に基づいており、クライアントが開始する一方向のリクエスト/レスポンスモデルのみをサポートしているため、リアルタイムのインタラクションの要件を満たすことができません。

Q:音声合成はテキスト文字数に基づいて課金されます。各合成のテキスト長を表示または取得するにはどうすればよいですか?

サーバーから返される result-generated イベントpayload.usage.characters パラメーターから文字数を取得できます。受信した最後の result-generated イベントの値が最終的なカウントになります。

トラブルシューティング

重要

コードエラーが発生した場合は、サーバーに送信された命令が正しいことを確認してください。命令の内容をプリントして、不正なフォーマットや必須パラメーターの欠落がないか確認できます。命令が正しい場合は、「エラーコード」の情報を使用して問題をトラブルシューティングしてください。

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

以下の 2 つの方法のいずれかで取得できます:

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

以下の手順に従って問題をトラブルシューティングしてください:

  1. 適用範囲が正しいことを確認してください。

  2. 機能を正しく呼び出していることを確認してください。詳細については、「SSML サポート」をご参照ください。

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

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

以下のシナリオに基づいてこの問題をトラブルシューティングしてください:

  1. 音声は .mp3 ファイルなどの完全なファイルとして保存されます。

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

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

  2. 音声はストリーミングモードで再生されます。

    1. 音声ストリームを完全なファイルとして保存し、再生を試みてください。ファイルの再生に失敗した場合は、最初のシナリオのトラブルシューティング手順をご参照ください。

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

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

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

以下のシナリオに基づいてこの問題をトラブルシューティングしてください:

  1. テキスト送信速度の確認: テキスト送信間隔が妥当であることを確認してください。前のセグメントの音声再生が終了した後に、次のテキストセグメントの送信が遅れないようにしてください。

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

    • コールバック関数に過剰なビジネスロジックが含まれていないか、ブロックの原因となっていないかを確認してください。

    • コールバック関数は WebSocket スレッドで実行されます。このスレッドがブロックされると、WebSocket がネットワークパケットを受信する能力が妨げられ、音声が途切れる原因となります。

    • WebSocket スレッドのブロックを避けるために、音声データを別の音声バッファーに書き込み、別のスレッドでそれを読み取って処理してください。

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

Q:音声合成が遅い (合成時間が長い) のはなぜですか?

以下のトラブルシューティング手順を実行してください:

  1. 入力間隔の確認

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

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

    • 最初のパケットの遅延:通常は約 500 ms です。

    • リアルタイム係数 (RTF):合計合成時間 / 音声の長さで計算されます。RTF は通常 1.0 未満です。

Q:合成された音声の発音が間違っている場合、どうすればよいですか?

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

Q:なぜ音声が返されないのですか? なぜテキストの末尾が音声に変換されないのですか? (合成音声の欠落)

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

Q:返された音声ストリームの順序が正しくなく、再生が乱れるのはなぜですか?

以下を確認して、この問題をトラブルシューティングしてください:

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

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

権限と認証

Q:API キーを CosyVoice 音声合成サービス専用にし、他の Model Studio モデルには使用したくない (権限の分離)。どうすればよいですか

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

その他の質問

詳細については、GitHub の Q&A をご参照ください。