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

Alibaba Cloud Model Studio:CosyVoice WebSocket API for speech synthesis

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

中国 (北京) リージョンのモデルを使用するには、中国 (北京) リージョンの API キーページに移動してください

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

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

ユーザーガイド:モデルの詳細とモデル選択のガイダンスについては、「リアルタイム音声合成 - 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

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

クライアントとサーバー間のインタラクションフローは次のとおりです:

  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, // 任意。Alibaba Cloud 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-v2",
        "parameters": {
            "text_type": "PlainText",
            "voice": "longxiaochun_v2",            // 音声
            "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 文字の英数字で構成される 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 命令を送信する際には、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-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 回しか送信できません。プレーンテキストまたは SSML を含むテキストがサポートされています。

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. 非中国語を指定する (クローン音声のみ)

    • フォーマット:「You will say it in <language>.」(注:末尾のピリオドを省略しないでください。「<language>」を特定の言語、例えば German に置き換えてください。)

    • 例:「You will say it in German.

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

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

    • フォーマット:「Say it in <dialect>.」(注:末尾のピリオドを省略しないでください。「<dialect>」を特定の方言、例えば Cantonese に置き換えてください。)

    • 例:「Say it in Cantonese.

    • サポートされている方言:広東語、東北語、甘粛語、貴州語、河南語、湖北語、江西語、閩南語、寧夏語、山西語、陝西語、山東語、上海語、四川語、天津語、雲南語。

  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 命令をサーバーに送信できます。そうしないと、タスクは失敗します。

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

クライアントによって生成された 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

現在までのリクエストで課金対象となる文字数。 単一のタスクでは、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。問題を特定するために、この ID を CosyVoice の開発者に提供できます。

payload パラメーター:

パラメーター

タイプ

説明

payload.usage.characters

integer

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

payload.output.sentence.index

integer

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

このフィールドと以下のフィールドは、word_timestamp_enabled を使用して単語レベルのタイムスタンプを有効にする必要があります。

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": 4,
                        "begin_time": 80,
                        "end_time": 200
                    },
                    {
                        "text": "is",
                        "begin_index": 5,
                        "end_index": 7,
                        "begin_time": 240,
                        "end_time": 360
                    },
                    {
                        "text": "the",
                        "begin_index": 8,
                        "end_index": 11,
                        "begin_time": 360,
                        "end_time": 480
                    },
                    {
                        "text": "weather",
                        "begin_index": 12,
                        "end_index": 19,
                        "begin_time": 480,
                        "end_time": 680
                    },
                    {
                        "text": "like",
                        "begin_index": 20,
                        "end_index": 24,
                        "begin_time": 680,
                        "end_time": 800
                    },
                    {
                        "text": "today",
                        "begin_index": 25,
                        "end_index": 30,
                        "begin_time": 800,
                        "end_time": 920
                    },
                    {
                        "text": "?",
                        "begin_index": 30,
                        "end_index": 31,
                        "begin_time": 920,
                        "end_time": 1320
                    }
                ]
            }
        },
        "usage": {"characters": 31}
    }
}

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 関数を呼び出すことによって行われます。

完全なサンプルを表示するにはクリック

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

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

  • cosyvoice-v2:longxiaochun_v2 などの音声を使用

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

Go

package main

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

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

const (
	wsURL      = "wss://dashscope.aliyuncs.com/api-ws/v1/inference/" // WebSocket サーバーエンドポイント。
	outputFile = "output.mp3"                                        // 出力ファイルパス。
)

func main() {
	// API キーを環境変数として設定していない場合は、次の行を apiKey := "your_api_key" に置き換えることができます。本番環境では、API キーの漏洩リスクを減らすため、コードに API キーをハードコーディングすることはお勧めしません。
	apiKey := os.Getenv("DASHSCOPE_API_KEY")
	// 出力ファイルをチェックしてクリアします。
	if err := clearOutputFile(outputFile); err != nil {
		fmt.Println("出力ファイルのクリアに失敗しました: ", err)
		return
	}

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

	// 結果を受信するためのゴルーチンを開始します。
	done, taskStarted := startResultReceiver(conn)

	// run-task 命令を送信します。
	taskID, err := sendRunTaskCmd(conn)
	if err != nil {
		fmt.Println("run-task 命令の送信に失敗しました: ", err)
		return
	}

	// task-started イベントを待ちます。
	for !*taskStarted {
		time.Sleep(100 * time.Millisecond)
	}

	// 合成するテキストを送信します。
	if err := sendContinueTaskCmd(conn, taskID); err != nil {
		fmt.Println("合成するテキストの送信に失敗しました: ", err)
		return
	}

	// finish-task 命令を送信します。
	if err := sendFinishTaskCmd(conn, taskID); err != nil {
		fmt.Println("finish-task 命令の送信に失敗しました: ", err)
		return
	}

	// 結果を受信するゴルーチンが完了するのを待ちます。
	<-done
}

var dialer = websocket.DefaultDialer

// JSON データを表す構造体を定義します。
type Header struct {
	Action       string                 `json:"action"`
	TaskID       string                 `json:"task_id"`
	Streaming    string                 `json:"streaming"`
	Event        string                 `json:"event"`
	ErrorCode    string                 `json:"error_code,omitempty"`
	ErrorMessage string                 `json:"error_message,omitempty"`
	Attributes   map[string]interface{} `json:"attributes"`
}

type Payload struct {
	TaskGroup  string     `json:"task_group"`
	Task       string     `json:"task"`
	Function   string     `json:"function"`
	Model      string     `json:"model"`
	Parameters Params     `json:"parameters"`
	Resources  []Resource `json:"resources"`
	Input      Input      `json:"input"`
}

type Params struct {
	TextType   string `json:"text_type"`
	Voice      string `json:"voice"`
	Format     string `json:"format"`
	SampleRate int    `json:"sample_rate"`
	Volume     int    `json:"volume"`
	Rate       int    `json:"rate"`
	Pitch      int    `json:"pitch"`
}

type Resource struct {
	ResourceID   string `json:"resource_id"`
	ResourceType string `json:"resource_type"`
}

type Input struct {
	Text string `json:"text"`
}

type Event struct {
	Header  Header  `json:"header"`
	Payload Payload `json:"payload"`
}

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

// run-task 命令を送信します。
func sendRunTaskCmd(conn *websocket.Conn) (string, error) {
	runTaskCmd, taskID, err := generateRunTaskCmd()
	if err != nil {
		return "", err
	}
	err = conn.WriteMessage(websocket.TextMessage, []byte(runTaskCmd))
	return taskID, err
}

// run-task 命令を生成します。
func generateRunTaskCmd() (string, string, error) {
	taskID := uuid.New().String()
	runTaskCmd := Event{
		Header: Header{
			Action:    "run-task",
			TaskID:    taskID,
			Streaming: "duplex",
		},
		Payload: Payload{
			TaskGroup: "audio",
			Task:      "tts",
			Function:  "SpeechSynthesizer",
			Model:     "cosyvoice-v3-flash",
			Parameters: Params{
				TextType:   "PlainText",
				Voice:      "longanyang",
				Format:     "mp3",
				SampleRate: 22050,
				Volume:     50,
				Rate:       1,
				Pitch:      1,
			},
			Input: Input{},
		},
	}
	runTaskCmdJSON, err := json.Marshal(runTaskCmd)
	return string(runTaskCmdJSON), taskID, err
}

// 合成するテキストを送信します。
func sendContinueTaskCmd(conn *websocket.Conn, taskID string) error {
	texts := []string{"静かな夜の思い、", "ベッドの前に月明かりが見える。", "頭を上げて月を見る。", "頭を下げて故郷を思う。"}

	for _, text := range texts {
		runTaskCmd, err := generateContinueTaskCmd(text, taskID)
		if err != nil {
			return err
		}

		err = conn.WriteMessage(websocket.TextMessage, []byte(runTaskCmd))
		if err != nil {
			return err
		}
	}

	return nil
}

// continue-task 命令を生成します。
func generateContinueTaskCmd(text string, taskID string) (string, error) {
	runTaskCmd := Event{
		Header: Header{
			Action:    "continue-task",
			TaskID:    taskID,
			Streaming: "duplex",
		},
		Payload: Payload{
			Input: Input{
				Text: text,
			},
		},
	}
	runTaskCmdJSON, err := json.Marshal(runTaskCmd)
	return string(runTaskCmdJSON), err
}

// 結果を受信するためのゴルーチンを開始します。
func startResultReceiver(conn *websocket.Conn) (chan struct{}, *bool) {
	done := make(chan struct{})
	taskStarted := new(bool)
	*taskStarted = false

	go func() {
		defer close(done)
		for {
			msgType, message, err := conn.ReadMessage()
			if err != nil {
				fmt.Println("サーバーメッセージの解析に失敗しました: ", err)
				return
			}

			if msgType == websocket.BinaryMessage {
				// バイナリ音声ストリームを処理します。
				if err := writeBinaryDataToFile(message, outputFile); err != nil {
					fmt.Println("バイナリデータの書き込みに失敗しました: ", err)
					return
				}
			} else {
				// テキストメッセージを処理します。
				var event Event
				err = json.Unmarshal(message, &event)
				if err != nil {
					fmt.Println("イベントの解析に失敗しました: ", err)
					continue
				}
				if handleEvent(conn, event, taskStarted) {
					return
				}
			}
		}
	}()

	return done, taskStarted
}

// イベントを処理します。
func handleEvent(conn *websocket.Conn, event Event, taskStarted *bool) bool {
	switch event.Header.Event {
	case "task-started":
		fmt.Println("task-started イベントを受信しました")
		*taskStarted = true
	case "result-generated":
		// result-generated イベントを無視します。
		return false
	case "task-finished":
		fmt.Println("タスクが終了しました")
		return true
	case "task-failed":
		handleTaskFailed(event, conn)
		return true
	default:
		fmt.Printf("予期しないイベント: %v\n", event)
	}
	return false
}

// task-failed イベントを処理します。
func handleTaskFailed(event Event, conn *websocket.Conn) {
	if event.Header.ErrorMessage != "" {
		fmt.Printf("タスクが失敗しました: %s\n", event.Header.ErrorMessage)
	} else {
		fmt.Println("不明な理由でタスクが失敗しました")
	}
}

// 接続を閉じます。
func closeConnection(conn *websocket.Conn) {
	if conn != nil {
		conn.Close()
	}
}

// バイナリデータをファイルに書き込みます。
func writeBinaryDataToFile(data []byte, filePath string) error {
	file, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return err
	}
	defer file.Close()

	_, err = file.Write(data)
	if err != nil {
		return err
	}

	return nil
}

// finish-task 命令を送信します。
func sendFinishTaskCmd(conn *websocket.Conn, taskID string) error {
	finishTaskCmd, err := generateFinishTaskCmd(taskID)
	if err != nil {
		return err
	}
	err = conn.WriteMessage(websocket.TextMessage, []byte(finishTaskCmd))
	return err
}

// finish-task 命令を生成します。
func generateFinishTaskCmd(taskID string) (string, error) {
	finishTaskCmd := Event{
		Header: Header{
			Action:    "finish-task",
			TaskID:    taskID,
			Streaming: "duplex",
		},
		Payload: Payload{
			Input: Input{},
		},
	}
	finishTaskCmdJSON, err := json.Marshal(finishTaskCmd)
	return string(finishTaskCmdJSON), err
}

// 出力ファイルをクリアします。
func clearOutputFile(filePath string) error {
	file, err := os.OpenFile(filePath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return err
	}
	file.Close()
	return nil
}

C#

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

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

    // 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
            },
            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("タスクが失敗しました。");
                        _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 キーをハードコーディングすることはお勧めしません。
$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
            ],
            "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 キーの漏洩リスクを減らすため、コードに 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 // ピッチ
      },
      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 命令を送信します。
        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 }, \"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
                },
                "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 の代わりに WebSocket を使用します。WebSocket を使用すると、サーバーとクライアントがリアルタイムの音声合成や認識の進捗をプッシュするなど、双方向でデータをアクティブに交換できます。対照的に、HTTP ベースの RESTful API は、クライアントが開始する一方向のリクエスト/レスポンスモデルのみをサポートしており、リアルタイムのインタラクションには適していません。

Q:音声合成は文字数に基づいて課金されます。各合成タスクの文字数を確認するにはどうすればよいですか?

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

トラブルシューティング

重要

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

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

リクエスト ID は 2 つの方法で取得できます:

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

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

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

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

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

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:返された音声ストリームのセグメントが順不同で、再生が乱れるのはなぜですか?

以下の 2 点を確認してください:

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

  • 非同期操作が、受信した順序とは異なる順序で音声データを書き込んでいないか確認してください。

権限と認証

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

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

その他の質問

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