WebSocket 接続を介して、CosyVoice のリアルタイム音声合成サービスにアクセスします。このドキュメントでは、サービスエンドポイント、リクエストヘッダー、およびクライアントとサーバー間のインタラクションワークフローについて説明します。
レート制限:モデルの呼び出しにはレート制限が適用されます。制限を超えると、サーバーはエラー Requests rate limit exceeded, please try again later. を返します。リクエストレートまたは同時実行数を減らしてリトライしてください。各モデルのレート制限については、「レート制限」をご参照ください。
サービスエンドポイント
固定の WebSocket URL:
シンガポール
wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference
{WorkspaceId} を実際のワークスペース ID に置き換えてください。
中国 (北京)
wss://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference
{WorkspaceId} を実際のワークスペース ID に置き換えてください。
重要Alibaba Cloud Model Studio は、中国 (北京) およびシンガポールリージョン向けにワークスペース固有のドメインをリリースしました。新しい専用ドメインは、推論リクエストに対して優れたパフォーマンスと高い安定性を提供します。新しいドメインへの移行を推奨します:
- 中国 (北京):
dashscope.aliyuncs.comから{WorkspaceId}.cn-beijing.maas.aliyuncs.comへ - シンガポール:
dashscope-intl.aliyuncs.comから{WorkspaceId}.ap-southeast-1.maas.aliyuncs.comへ
リクエストヘッダー
リクエストに次のヘッダーを含めてください:
| パラメーター | タイプ | 必須 | 説明 |
|---|---|---|---|
| Authorization | 文字列 | はい | フォーマット:Bearer <API key>。<API key> をご自身の API キーに置き換えてください。 |
| user-agent | 文字列 | いいえ | リクエスト追跡用のクライアント識別子。 |
| X-DashScope-WorkSpace | 文字列 | いいえ | Alibaba Cloud Model Studio のワークスペース ID。 |
| X-DashScope-DataInspection | 文字列 | いいえ | データインスペクションを有効にします。デフォルト:enable。特に必要がない限り、設定しないでください。 |
重要権限付与は WebSocket ハンドシェイク中に検証されます。API キーが無効または欠落している場合、ハンドシェイクは HTTP 401/403 エラーで失敗します。
インタラクションワークフロー
クライアント側およびサーバー側のイベントの詳細については、「クライアントイベント」と「サーバー側イベント」をご参照ください。
クライアントとサーバーのインタラクションシーケンス:
-
接続の確立:サーバーへの WebSocket 接続を作成します。
-
タスクの開始:run-task イベントを送信します。
-
確認の待機:次に進む前に、サーバーから task-started イベントを受信します。
-
合成用テキストの送信:
合成するテキストを含む continue-task イベントを、順番に 1 つ以上送信します。サーバーは、完全な文ごとに音声ストリームを含む result-generated イベントを返します。テキスト長の制限については、continue-task イベントの
textフィールドをご参照ください。注記テキストセグメントを含む複数の continue-task イベントを順番に送信します。サーバーはテキストを文に分割します:
- 完全な文は直ちに音声に合成され、返されます。
- 不完全な文は、完了するまでバッファリングされます。
finish-task イベントにより、バッファリングされたすべてのコンテンツが強制的に合成されます。
-
音声の受信:
binaryチャンネルから音声ストリームを読み取ります。 -
タスクの終了:
すべてのテキストを送信した後、finish-task イベントを送信し、音声の受信を続けます。このステップは必須です。スキップすると、不完全な音声が生成される可能性があります。
-
タスク完了の受信:
サーバーからの task-finished イベントにより、タスクが完了したことが確認されます。
-
接続の終了:WebSocket 接続を切断します。
タスクごとに新しい接続を作成するのではなく、複数のタスクで WebSocket 接続を再利用してください。
重要1 つの合成タスク内のすべてのイベント (run-task、continue-task、finish-task) は、同じ task_id を共有する必要があります。タスクごとに新しい task_id (UUID など) を生成してください。値が一致しない場合、音声の破損やタスクの失敗を引き起こす可能性があります。
コード例
次の例は汎用 WebSocket ライブラリでイベントを直接送信し、cosyvoice-v3-flash と音声 longanyang を使用します。API キーを取得して環境変数 DASHSCOPE_API_KEY に設定し、{WorkspaceId} を対象リージョンのワークスペース ID に置き換えてください。リージョンを切り替える際は、エンドポイントと API キーの両方を変更します。
Java
開発には Java DashScope SDK の使用を推奨します。「CosyVoice Java SDK」をご参照ください。
以下は Java WebSocket 直接接続の例です。実行前に以下の依存関係をインポートしてください:
Java-WebSocketjackson-databind
Maven または Gradle を使用して依存関係を管理します:
<dependencies>
<!-- WebSocket Client -->
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.5.3</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.0</version>
</dependency>
</dependencies>
// 他のコードは省略
dependencies {
// WebSocket Client
implementation 'org.java-websocket:Java-WebSocket:1.5.3'
// JSON Processing
implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.0'
}
// 他のコードは省略
Java コード:
import com.fasterxml.jackson.databind.ObjectMapper;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.*;
public class TTSWebSocketClient extends WebSocketClient {
private final String taskId = UUID.randomUUID().toString();
private final String outputFile = "output_" + System.currentTimeMillis() + ".mp3";
private boolean taskFinished = false;
public TTSWebSocketClient(URI serverUri, Map<String, String> headers) {
super(serverUri, headers);
}
@Override
public void onOpen(ServerHandshake serverHandshake) {
System.out.println("Connection established");
// 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("Received message from server: " + 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("Received task-started event from server");
List<String> texts = Arrays.asList(
"Before my bed, moonlight shines bright,I suspect it\'s frost upon the ground.",
"I raise my eyes to gaze at the bright moon,then bow my head, thinking of home."
);
for (String text : texts) {
// continue-task イベントを送信します
sendContinueTask(text);
}
// finish-task イベントを送信します
sendFinishTask();
} else if ("task-finished".equals(event)) {
System.out.println("Received task-finished event from server");
taskFinished = true;
closeConnection();
} else if ("task-failed".equals(event)) {
System.out.println("Task failed: " + message);
closeConnection();
}
}
}
} catch (Exception e) {
System.err.println("Exception occurred: " + e.getMessage());
}
}
@Override
public void onMessage(ByteBuffer message) {
System.out.println("Received binary audio data of size: " + message.remaining());
try (FileOutputStream fos = new FileOutputStream(outputFile, true)) {
byte[] buffer = new byte[message.remaining()];
message.get(buffer);
fos.write(buffer);
System.out.println("Audio data written to local file " + outputFile);
} catch (IOException e) {
System.err.println("Failed to write audio data to local file: " + e.getMessage());
}
}
@Override
public void onClose(int code, String reason, boolean remote) {
System.out.println("Connection closed: " + reason + " (" + code + ")");
}
@Override
public void onError(Exception ex) {
System.err.println("Error: " + ex.getMessage());
ex.printStackTrace();
}
private void sendContinueTask(String text) {
String command = "{ \"header\": { \"action\": \"continue-task\", \"task_id\": \"" + taskId + "\", \"streaming\": \"duplex\" }, \"payload\": { \"input\": { \"text\": \"" + text + "\" } }}";
send(command);
}
private void sendFinishTask() {
String command = "{ \"header\": { \"action\": \"finish-task\", \"task_id\": \"" + taskId + "\", \"streaming\": \"duplex\" }, \"payload\": { \"input\": {} }}";
send(command);
}
private void closeConnection() {
if (!isClosed()) {
close();
}
}
public static void main(String[] args) {
try {
// API キーはシンガポールリージョンと中国 (北京) リージョンで異なります。API キーの取得: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// 環境変数を設定していない場合は、次の行を中国 Model Studio API キーに置き換えてください: String apiKey = "sk-xxx"
String apiKey = System.getenv("DASHSCOPE_API_KEY");
if (apiKey == null || apiKey.isEmpty()) {
System.err.println("Please set the DASHSCOPE_API_KEY environment variable");
return;
}
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "bearer " + apiKey);
// 以下はシンガポールリージョンの構成です。{WorkspaceId} を実際のワークスペース ID に置き換えてください。リージョンによって構成は異なります。
TTSWebSocketClient client = new TTSWebSocketClient(new URI("wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference"), headers);
client.connect();
while (!client.isClosed() && !client.taskFinished) {
Thread.sleep(1000);
}
} catch (Exception e) {
System.err.println("Failed to connect to WebSocket service: " + e.getMessage());
e.printStackTrace();
}
}
}
Python
開発には Python DashScope SDK の使用を推奨します。「CosyVoice 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 サービス URL
"""
self.api_key = api_key # ご自身の API キーに置き換えてください
self.uri = uri # ご自身の WebSocket URL に置き換えてください
self.task_id = str(uuid.uuid4()) # ユニークなタスク ID を生成します
self.output_file = f"output_{int(time.time())}.mp3" # 出力オーディオファイルのパス
self.ws = None # WebSocketApp インスタンス
self.task_started = False # task-started を受信したかどうか
self.task_finished = False # task-finished / task-failed を受信したかどうか
def on_open(self, ws):
"""
WebSocket 接続が確立されたときのコールバックです。
音声合成タスクを開始するための run-task イベントを送信します。
"""
print("WebSocket connected")
# run-task イベントを構築します
run_task_cmd = {
"header": {
"action": "run-task",
"task_id": self.task_id,
"streaming": "duplex"
},
"payload": {
"task_group": "audio",
"task": "tts",
"function": "SpeechSynthesizer",
"model": "cosyvoice-v3-flash",
"parameters": {
"text_type": "PlainText",
"voice": "longanyang",
"format": "mp3",
"sample_rate": 22050,
"volume": 50,
"rate": 1,
"pitch": 1,
# enable_ssml を true に設定すると、continue-task イベントは 1 つしか送信できません。それ以外の場合はエラーが返されます。
"enable_ssml": False
},
"input": {}
}
}
# run-task イベントを送信します
ws.send(json.dumps(run_task_cmd))
print("Sent run-task event")
def on_message(self, ws, message):
"""
メッセージを受信したときのコールバックです。
テキストメッセージとバイナリメッセージを別々に処理します。
"""
if isinstance(message, str):
# JSON テキストメッセージを処理します
try:
msg_json = json.loads(message)
print(f"Received JSON message: {msg_json}")
if "header" in msg_json:
header = msg_json["header"]
if "event" in header:
event = header["event"]
if event == "task-started":
print("Task started")
self.task_started = True
# continue-task イベントを送信します
texts = [
"Before my bed, moonlight shines bright,I suspect it\'s frost upon the ground.",
"I raise my eyes to gaze at the bright moon,then bow my head, thinking of home."
]
for text in texts:
self.send_continue_task(text)
# すべての continue-task イベントを送信した後に finish-task を送信します
self.send_finish_task()
elif event == "task-finished":
print("Task completed")
self.task_finished = True
self.close(ws)
elif event == "task-failed":
error_msg = msg_json.get("header", {}).get("error_message", "Unknown error")
print(f"Task failed: {error_msg}")
self.task_finished = True
self.close(ws)
except json.JSONDecodeError as e:
print(f"JSON parsing failed: {e}")
else:
# バイナリメッセージ (オーディオデータ) を処理します
print(f"Received binary message, size: {len(message)} bytes")
with open(self.output_file, "ab") as f:
f.write(message)
print(f"Audio data written to local file {self.output_file}")
def on_error(self, ws, error):
"""エラーが発生したときのコールバックです。"""
print(f"WebSocket error: {error}")
def on_close(self, ws, close_status_code, close_msg):
"""接続が閉じられたときのコールバックです。"""
print(f"WebSocket closed: {close_msg} ({close_status_code})")
def send_continue_task(self, text):
"""合成対象のテキストコンテンツを含む continue-task イベントを送信します。"""
cmd = {
"header": {
"action": "continue-task",
"task_id": self.task_id,
"streaming": "duplex"
},
"payload": {
"input": {
"text": text
}
}
}
self.ws.send(json.dumps(cmd))
print(f"Sent continue-task event, text content: {text}")
def send_finish_task(self):
"""音声合成タスクを終了する finish-task イベントを送信します。"""
cmd = {
"header": {
"action": "finish-task",
"task_id": self.task_id,
"streaming": "duplex"
},
"payload": {
"input": {}
}
}
self.ws.send(json.dumps(cmd))
print("Sent finish-task event")
def close(self, ws):
"""接続をアクティブに閉じます。"""
if ws and ws.sock and ws.sock.connected:
ws.close()
print("Connection closed actively")
def run(self):
"""WebSocket クライアントを開始します。"""
# リクエストヘッダーを設定します (認証)
header = {
"Authorization": f"bearer {self.api_key}",
"X-DashScope-DataInspection": "enable"
}
# WebSocketApp インスタンスを作成します
self.ws = websocket.WebSocketApp(
self.uri,
header=header,
on_open=self.on_open,
on_message=self.on_message,
on_error=self.on_error,
on_close=self.on_close
)
print("Listening for WebSocket messages...")
self.ws.run_forever() # 持続的接続リスナーを開始します
# 使用例
if __name__ == "__main__":
# API キーはシンガポールリージョンと北京リージョンで異なります。API キーの取得: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
# 環境変数を設定していない場合は、次の行を中国語版 Model Studio の API キーに置き換えてください: API_KEY = "sk-xxx"
API_KEY = os.environ.get("DASHSCOPE_API_KEY")
# 以下はシンガポールリージョンの設定です。{WorkspaceId} を実際のワークスペース ID に置き換えてください。設定はリージョンによって異なります。
SERVER_URI = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference" # ご自身の WebSocket URL に置き換えてください
client = TTSClient(API_KEY, SERVER_URI)
client.run()
Go
go mod init tts-example
go get github.com/gorilla/websocket@v1.5.3 github.com/google/uuid@v1.6.0
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"time"
"github.com/google/uuid"
"github.com/gorilla/websocket"
)
const (
// 以下はシンガポールリージョンの構成です。{WorkspaceId} を実際のワークスペース ID に置き換えてください。リージョンによって構成は異なります。
wsURL = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference"
outputFile = "output.mp3"
)
func main() {
// API キーはシンガポールリージョンと中国 (北京) リージョンで異なります。API キーの取得: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// 環境変数を設定していない場合は、次の行を中国 Model Studio API キーに置き換えてください: apiKey := "sk-xxx"
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("Connection failed, HTTP status code: %d\n", resp.StatusCode)
}
fmt.Println("Connection failed:", err)
return
}
defer conn.Close()
// タスク ID を生成します
taskID := uuid.New().String()
fmt.Printf("Generated task 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("Sending run-task event: %s\n", string(runTaskJSON))
err = conn.WriteMessage(websocket.TextMessage, runTaskJSON)
if err != nil {
fmt.Println("Failed to send run-task:", err)
return
}
textSent := false
// メッセージを処理します
for {
messageType, message, err := conn.ReadMessage()
if err != nil {
fmt.Println("Failed to read message:", err)
break
}
// バイナリメッセージを処理します
if messageType == websocket.BinaryMessage {
fmt.Printf("Received binary message, length: %d\n", len(message))
file, _ := os.OpenFile(outputFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
file.Write(message)
file.Close()
continue
}
// テキストメッセージを処理します
messageStr := string(message)
fmt.Printf("Received text message: %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("Event type: %s\n", event)
switch event {
case "task-started":
fmt.Println("=== Received task-started event ===")
if !textSent {
// continue-task イベントを送信します
texts := []string{"Before my bed, moonlight shines bright, I suspect it's frost upon the ground.", "I raise my eyes to gaze at the bright moon, then bow my head, thinking of home."}
for _, text := range texts {
continueTaskCmd := map[string]interface{}{
"header": map[string]interface{}{
"action": "continue-task",
"task_id": taskID,
"streaming": "duplex",
},
"payload": map[string]interface{}{
"input": map[string]interface{}{
"text": text,
},
},
}
continueTaskJSON, _ := json.Marshal(continueTaskCmd)
fmt.Printf("Sending continue-task event: %s\n", string(continueTaskJSON))
err = conn.WriteMessage(websocket.TextMessage, continueTaskJSON)
if err != nil {
fmt.Println("Failed to send 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("Sending finish-task event: %s\n", string(finishTaskJSON))
err = conn.WriteMessage(websocket.TextMessage, finishTaskJSON)
if err != nil {
fmt.Println("Failed to send finish-task:", err)
return
}
}
case "task-finished":
fmt.Println("=== Task completed ===")
return
case "task-failed":
fmt.Println("=== Task failed ===")
if header["error_message"] != nil {
fmt.Printf("Error message: %s\n", header["error_message"])
}
return
case "result-generated":
fmt.Println("Received result-generated event")
}
}
}
}
}
}
C#
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
class Program {
// API キーはシンガポールリージョンと中国 (北京) リージョンで異なります。API キーの取得: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// 環境変数を設定していない場合は、次の行を中国 Model Studio API キーに置き換えてください: private static readonly string ApiKey = "sk-xxx"
private static readonly string ApiKey = Environment.GetEnvironmentVariable("DASHSCOPE_API_KEY") ?? throw new InvalidOperationException("DASHSCOPE_API_KEY environment variable is not set.");
// 以下はシンガポールリージョンの構成です。{WorkspaceId} を実際のワークスペース ID に置き換えてください。リージョンによって構成は異なります。
private const string WebSocketUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.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 = {
"Before my bed, moonlight shines bright,",
"I suspect it\'s frost upon the ground.",
"I raise my eyes to gaze at the bright moon,",
"then bow my head, thinking of home."
};
foreach (string text in texts) {
await SendContinueTaskCommandAsync(text);
}
// finish-task イベントを送信します
await SendFinishTaskCommandAsync(_taskId);
// 受信タスクの完了を待ちます
await receiveTask;
Console.WriteLine("Task completed, connection closed.");
} catch (OperationCanceledException) {
Console.WriteLine("Task cancelled.");
} catch (Exception ex) {
Console.WriteLine($"Error occurred: {ex.Message}");
} finally {
_cancellationTokenSource.Cancel();
_webSocket.Dispose();
}
}
private static void ClearOutputFile(string filePath) {
if (File.Exists(filePath)) {
File.WriteAllText(filePath, string.Empty);
Console.WriteLine("Output file cleared.");
} else {
Console.WriteLine("Output file does not exist, no need to clear.");
}
}
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("Successfully connected to the WebSocket service.");
} catch (OperationCanceledException) {
Console.WriteLine("WebSocket connection cancelled.");
} catch (Exception ex) {
Console.WriteLine($"WebSocket connection failed: {ex.Message}");
throw;
}
}
private static async Task SendRunTaskCommandAsync(string taskId) {
var command = CreateCommand("run-task", taskId, "duplex", new {
task_group = "audio",
task = "tts",
function = "SpeechSynthesizer",
model = "cosyvoice-v3-flash",
parameters = new
{
text_type = "PlainText",
voice = "longanyang",
format = "mp3",
sample_rate = 22050,
volume = 50,
rate = 1,
pitch = 1,
// enable_ssml を true に設定すると、continue-task イベントを 1 回しか送信できません。それ以外の場合、「Text request limit violated, expected 1.」というエラーが返されます
enable_ssml = false
},
input = new { }
});
await SendJsonMessageAsync(command);
Console.WriteLine("run-task event sent.");
}
private static async Task SendContinueTaskCommandAsync(string text) {
if (_taskId == null) {
throw new InvalidOperationException("Task ID not initialized.");
}
var command = CreateCommand("continue-task", _taskId, "duplex", new {
input = new {
text
}
});
await SendJsonMessageAsync(command);
Console.WriteLine("continue-task event sent.");
}
private static async Task SendFinishTaskCommandAsync(string taskId) {
var command = CreateCommand("finish-task", taskId, "duplex", new {
input = new { }
});
await SendJsonMessageAsync(command);
Console.WriteLine("finish-task event sent.");
}
private static async Task SendJsonMessageAsync(string message) {
var buffer = Encoding.UTF8.GetBytes(message);
try {
await _webSocket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, _cancellationTokenSource.Token);
} catch (OperationCanceledException) {
Console.WriteLine("Message sending cancelled.");
}
}
private static async Task ReceiveMessagesAsync() {
while (_webSocket.State == WebSocketState.Open) {
var response = await ReceiveMessageAsync();
if (response != null) {
var eventStr = response.RootElement.GetProperty("header").GetProperty("event").GetString();
switch (eventStr) {
case "task-started":
Console.WriteLine("Task started.");
_taskStartedTcs.TrySetResult(true);
break;
case "task-finished":
Console.WriteLine("Task completed.");
_cancellationTokenSource.Cancel();
break;
case "task-failed":
Console.WriteLine("Task failed: " + response.RootElement.GetProperty("header").GetProperty("error_message").GetString());
_cancellationTokenSource.Cancel();
break;
default:
// 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("Received binary data...");
// バイナリデータをファイルに保存します
using (var fileStream = new FileStream(OutputFilePath, FileMode.Append)) {
fileStream.Write(buffer, 0, result.Count);
}
return null;
}
string message = Encoding.UTF8.GetString(buffer, 0, result.Count);
return JsonDocument.Parse(message);
} catch (OperationCanceledException) {
Console.WriteLine("Message receiving cancelled.");
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 キーの取得: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// 環境変数を設定していない場合は、次の行を中国 Model Studio API キーに置き換えてください: $api_key = "sk-xxx"
$api_key = getenv("DASHSCOPE_API_KEY");
// 以下はシンガポールリージョンの構成です。{WorkspaceId} を実際のワークスペース ID に置き換えてください。リージョンによって構成は異なります。
$websocket_url = 'wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'; // WebSocket サーバー URL
$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' => true,
'verify_peer_name' => true,
],
]);
$connector = new Connector($loop, $socketConnector);
$headers = [
'Authorization' => 'bearer ' . $api_key,
'X-DashScope-DataInspection' => 'enable'
];
$connector($websocket_url, [], $headers)->then(function ($conn) use ($loop, $output_file) {
echo "Connected to the WebSocket server\n";
// タスク ID を生成します
$taskId = generateTaskId();
// run-task イベントを送信します
sendRunTaskMessage($conn, $taskId);
// continue-task イベントを送信する関数を定義します
$sendContinueTask = function() use ($conn, $loop, $taskId) {
// 送信するテキスト
$texts = ["Before my bed, moonlight shines bright,", "I suspect it\'s frost upon the ground.", "I raise my eyes to gaze at the bright moon,", "then bow my head, thinking of home."];
$continueTaskCount = 0;
foreach ($texts as $text) {
$continueTaskMessage = json_encode([
"header" => [
"action" => "continue-task",
"task_id" => $taskId,
"streaming" => "duplex"
],
"payload" => [
"input" => [
"text" => $text
]
]
]);
echo "Sending continue-task event: " . $continueTaskMessage . "\n";
$conn->send($continueTaskMessage);
$continueTaskCount++;
}
echo "Number of continue-task events sent: " . $continueTaskCount . "\n";
// finish-task イベントを送信します
sendFinishTaskMessage($conn, $taskId);
};
// task-started イベントを受信したかどうかのフラグ
$taskStarted = false;
// メッセージをリッスンします
$conn->on('message', function($msg) use ($conn, $sendContinueTask, $loop, &$taskStarted, $taskId, $output_file) {
if ($msg->isBinary()) {
// バイナリデータをローカルファイルに書き込みます
file_put_contents($output_file, $msg->getPayload(), FILE_APPEND);
} else {
// バイナリ以外のメッセージを処理します
$response = json_decode($msg, true);
if (isset($response['header']['event'])) {
handleEvent($conn, $response, $sendContinueTask, $loop, $taskId, $taskStarted);
} else {
echo "Unknown message format\n";
}
}
});
// 接続クローズをリッスンします
$conn->on('close', function($code = null, $reason = null) {
echo "Connection closed\n";
if ($code !== null) {
echo "Close code: " . $code . "\n";
}
if ($reason !== null) {
echo "Close reason: " . $reason . "\n";
}
});
}, function ($e) {
echo "Unable to connect: {$e->getMessage()}\n";
});
$loop->run();
/**
* タスク ID を生成します
* @return string
*/
function generateTaskId(): string {
return bin2hex(random_bytes(16));
}
/**
* run-task イベントを送信します
* @param $conn
* @param $taskId
*/
function sendRunTaskMessage($conn, $taskId) {
$runTaskMessage = json_encode([
"header" => [
"action" => "run-task",
"task_id" => $taskId,
"streaming" => "duplex"
],
"payload" => [
"task_group" => "audio",
"task" => "tts",
"function" => "SpeechSynthesizer",
"model" => "cosyvoice-v3-flash",
"parameters" => [
"text_type" => "PlainText",
"voice" => "longanyang",
"format" => "mp3",
"sample_rate" => 22050,
"volume" => 50,
"rate" => 1,
"pitch" => 1,
// enable_ssml を true に設定すると、continue-task イベントを 1 回しか送信できません。それ以外の場合、「Text request limit violated, expected 1.」というエラーが返されます
"enable_ssml" => false
],
"input" => (object) []
]
]);
echo "Sending run-task event: " . $runTaskMessage . "\n";
$conn->send($runTaskMessage);
echo "run-task event sent\n";
}
/**
* オーディオファイルを読み込みます
* @param string $filePath
* @return bool|string
*/
function readAudioFile(string $filePath) {
$voiceData = file_get_contents($filePath);
if ($voiceData === false) {
echo "Unable to read audio file\n";
}
return $voiceData;
}
/**
* オーディオデータを分割します
* @param string $data
* @param int $chunkSize
* @return array
*/
function splitAudioData(string $data, int $chunkSize): array {
return str_split($data, $chunkSize);
}
/**
* finish-task イベントを送信します
* @param $conn
* @param $taskId
*/
function sendFinishTaskMessage($conn, $taskId) {
$finishTaskMessage = json_encode([
"header" => [
"action" => "finish-task",
"task_id" => $taskId,
"streaming" => "duplex"
],
"payload" => [
"input" => (object) []
]
]);
echo "Sending finish-task event: " . $finishTaskMessage . "\n";
$conn->send($finishTaskMessage);
echo "finish-task event sent\n";
}
/**
* イベントを処理します
* @param $conn
* @param $response
* @param $sendContinueTask
* @param $loop
* @param $taskId
* @param $taskStarted
*/
function handleEvent($conn, $response, $sendContinueTask, $loop, $taskId, &$taskStarted) {
switch ($response['header']['event']) {
case 'task-started':
echo "Task started, sending continue-task events...\n";
$taskStarted = true;
// continue-task イベントを送信します
$sendContinueTask();
break;
case 'result-generated':
// result-generated イベントを受信しました
break;
case 'task-finished':
echo "Task completed\n";
$conn->close();
break;
case 'task-failed':
echo "Task failed\n";
echo "Error code: " . $response['header']['error_code'] . "\n";
echo "Error message: " . $response['header']['error_message'] . "\n";
$conn->close();
break;
case 'error':
echo "Error: " . $response['payload']['message'] . "\n";
break;
default:
echo "Unknown event: " . $response['header']['event'] . "\n";
break;
}
// タスクが完了した場合、接続をクローズします
if ($response['header']['event'] == 'task-finished') {
// すべてのデータが送信されたことを確認するために 1 秒待ちます
$loop->addTimer(1, function() use ($conn) {
$conn->close();
echo "Client closing connection\n";
});
}
// task-started イベントを受信していない場合、タスクが失敗またはエラーになったら接続をクローズします
if (!$taskStarted && in_array($response['header']['event'], ['task-failed', 'error'])) {
$conn->close();
}
}
Node.js
必要な依存関係をインストールします:
npm install ws
npm install uuid
サンプルコード:
const WebSocket = require('ws');
const fs = require('fs');
const uuid = require('uuid').v4;
// API キーはシンガポールリージョンと中国 (北京) リージョンで異なります。API キーの取得: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// 環境変数を設定していない場合は、次の行を中国 Model Studio API キーに置き換えてください: const apiKey = "sk-xxx"
const apiKey = process.env.DASHSCOPE_API_KEY;
// 以下はシンガポールリージョンの構成です。{WorkspaceId} を実際のワークスペース ID に置き換えてください。リージョンによって構成は異なります。
const url = 'wss://{WorkspaceId}.ap-southeast-1.maas.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('Connected to the WebSocket server');
// 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 message sent');
});
const fileStream = fs.createWriteStream(outputFilePath, { flags: 'a' });
ws.on('message', (data, isBinary) => {
if (isBinary) {
// バイナリデータをファイルに書き込みます
fileStream.write(data);
} else {
const message = JSON.parse(data);
switch (message.header.event) {
case 'task-started':
taskStarted = true;
console.log('Task started');
// continue-task イベントを送信します
sendContinueTasks(ws);
break;
case 'task-finished':
console.log('Task completed');
ws.close();
fileStream.end(() => {
console.log('File stream closed');
});
break;
case 'task-failed':
console.error('Task failed: ', message.header.error_message);
ws.close();
fileStream.end(() => {
console.log('File stream closed');
});
break;
default:
// result-generated はここで処理できます
break;
}
}
});
function sendContinueTasks(ws) {
const texts = [
'Before my bed, moonlight shines bright,',
'I suspect it is frost upon the ground.',
'I raise my eyes to gaze at the bright moon,',
'then bow my head, thinking of home.'
];
texts.forEach((text, index) => {
setTimeout(() => {
if (taskStarted) {
const continueTaskMessage = JSON.stringify({
header: {
action: 'continue-task',
task_id: taskId,
streaming: 'duplex'
},
payload: {
input: {
text: text
}
}
});
ws.send(continueTaskMessage);
console.log(`continue-task sent, text: ${text}`);
}
}, index * 1000); // 1 秒ごとに送信します
});
// finish-task イベントを送信します
setTimeout(() => {
if (taskStarted) {
const finishTaskMessage = JSON.stringify({
header: {
action: 'finish-task',
task_id: taskId,
streaming: 'duplex'
},
payload: {
input: {}
}
});
ws.send(finishTaskMessage);
console.log('finish-task sent');
}
}, texts.length * 1000 + 1000); // すべての continue-task イベント送信後 1 秒で送信します
}
ws.on('close', () => {
console.log('Disconnected from the WebSocket server');
});