このトピックでは、WebSocket 接続を介して Qwen-Audio-ASR-Streaming リアルタイム音声認識サービスにアクセスするためのサービスエンドポイント、リクエストヘッダー、インタラクションフローについて説明します。
サービスエンドポイント
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 | string | はい | Bearer <your_api_key> 形式の認証トークン。<your_api_key> を実際の API キーに置き換えてください。 |
user-agent | string | いいえ | サーバーがリクエスト元を追跡するのに役立つクライアント識別子です。 |
X-DashScope-WorkSpace | string | いいえ | Model Studio の ワークスペース ID です。 |
X-DashScope-DataInspection | string | いいえ | データコンプライアンス検査を有効にするかどうかを指定します。このパラメーターは、省略した場合、デフォルトで enable に設定されます。必要がない限り、このパラメーターは有効にしないでください。 |
Authorization は WebSocket ハンドシェイク時に検証されます。API キーが無効または設定されていない場合、ハンドシェイクは失敗し、HTTP 401 または 403 エラーが返されます。
インタラクションフロー
クライアントイベントとサーバーサイドイベントの詳細については、「クライアントイベント」および「サーバーサイドイベント」をご参照ください。- 接続を確立します:クライアントはサーバーへの WebSocket 接続を確立します。
- タスクを開始します:クライアントは run-task 命令を送信してタスクを開始し、サーバーから task-started イベントを受信します。このイベントは、タスクが開始され、後続の手順に進めることを確認します。
- オーディオストリームを送信します:クライアントはバイナリオーディオ (モノラルである必要があります) の送信を開始し、同時にサーバーから result-generated イベントのストリームを受信します。各イベントには音声認識結果が含まれます。
- タスク終了をサーバーに通知します:クライアントは finish-task 命令を送信してタスク終了をサーバーに通知し、その後もサーバーから result-generated イベントを受信し続けます。
- タスクを終了します:クライアントはサーバーから task-finished イベントを受信します。これはタスクが終了したことを示します。
- 接続を閉じます:クライアントは WebSocket 接続を閉じます。
呼び出し例
以下の例では qwen-audio-3.1-asr-flash-streaming を使用します。コンソールで API キーと Base URL を取得し、環境変数DASHSCOPE_API_KEY と DASHSCOPE_BASE_URL に設定します。
サンプル音声を asr_example.wav として保存してください。16 kHz、モノラル、16-bit PCM エンコードの WAV ファイルです。
- Java
- Python
- Go
- Node.js
- C#
- PHP
例を実行する前に、Java-WebSocket 依存関係をインストールしてください:
Maven
Copy
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.5.6</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20240303</version>
</dependency>
Copy
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import org.json.JSONObject;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
public class FunASRRealtimeClient {
private static final String API_KEY = System.getenv("DASHSCOPE_API_KEY");
private static final String URL = "wss://" + URI.create(System.getenv("DASHSCOPE_BASE_URL")).getHost() + "/api-ws/v1/inference";
private static final String AUDIO_FILE = "asr_example.wav"; // Replace with the path to your audio file
private static final String MODEL = "qwen-audio-3.1-asr-flash-streaming";
// Generate a 32-character random ID
private static final String TASK_ID = UUID.randomUUID().toString().replace("-", "").substring(0, 32);
private static final AtomicBoolean taskStarted = new AtomicBoolean(false);
private static WebSocketClient client;
public static void main(String[] args) throws Exception {
client = new WebSocketClient(new URI(URL)) {
@Override
public void onOpen(ServerHandshake handshake) {
System.out.println("Connected to server");
sendRunTask();
}
@Override
public void onMessage(String data) {
JSONObject message = new JSONObject(data);
String event = message.getJSONObject("header").getString("event");
switch (event) {
case "task-started":
System.out.println("Task started");
taskStarted.set(true);
new Thread(FunASRRealtimeClient::sendAudioStream).start();
break;
case "result-generated":
JSONObject payload = message.getJSONObject("payload");
String text = payload.getJSONObject("output").getJSONObject("sentence").getString("text");
System.out.println("Recognition result: " + text);
if (payload.has("usage")) {
System.out.println("Audio duration (seconds): " + payload.getJSONObject("usage").get("duration"));
}
break;
case "task-finished":
System.out.println("Task finished");
close();
break;
case "task-failed":
String errMsg = message.getJSONObject("header").optString("error_message");
System.err.println("Task failed: " + errMsg);
close();
break;
default:
System.out.println("Unknown event: " + event);
}
}
@Override
public void onClose(int code, String reason, boolean remote) {
if (!taskStarted.get()) {
System.err.println("Task not started, closing connection");
}
}
@Override
public void onError(Exception ex) {
System.err.println("WebSocket error: " + ex.getMessage());
}
};
client.addHeader("Authorization", "bearer " + API_KEY);
client.connectBlocking();
}
// Send the run-task instruction
private static void sendRunTask() {
JSONObject runTask = new JSONObject()
.put("header", new JSONObject()
.put("action", "run-task")
.put("task_id", TASK_ID)
.put("streaming", "duplex"))
.put("payload", new JSONObject()
.put("task_group", "audio")
.put("task", "asr")
.put("function", "recognition")
.put("model", MODEL)
.put("parameters", new JSONObject()
.put("sample_rate", 16000)
.put("format", "wav"))
.put("input", new JSONObject()));
client.send(runTask.toString());
}
// Send the audio stream (send one binary chunk every 100ms)
private static void sendAudioStream() {
int chunkSize = 3200; // 100ms @ 16kHz 16bit mono
try {
byte[] audio = Files.readAllBytes(Paths.get(AUDIO_FILE));
int offset = 0;
while (offset < audio.length) {
int end = Math.min(offset + chunkSize, audio.length);
byte[] chunk = new byte[end - offset];
System.arraycopy(audio, offset, chunk, 0, end - offset);
client.send(ByteBuffer.wrap(chunk));
offset = end;
Thread.sleep(100);
}
System.out.println("Audio stream ended");
sendFinishTask();
} catch (Exception e) {
System.err.println("Error reading audio file: " + e.getMessage());
client.close();
}
}
// Send the finish-task instruction
private static void sendFinishTask() {
JSONObject finishTask = new JSONObject()
.put("header", new JSONObject()
.put("action", "finish-task")
.put("task_id", TASK_ID)
.put("streaming", "duplex"))
.put("payload", new JSONObject()
.put("input", new JSONObject()));
client.send(finishTask.toString());
}
}
例を実行する前に、以下のコマンドで依存関係をインストールしてください:
Copy
python -m pip install websocket-client
インポート時に
websocket モジュールと競合するため、サンプルファイルを websocket.py という名前にしないでください。Copy
# pip install websocket-client
import os
import json
import time
import uuid
import threading
from urllib.parse import urlsplit
import websocket
api_key = os.environ.get('DASHSCOPE_API_KEY')
url = f"wss://{urlsplit(os.environ['DASHSCOPE_BASE_URL']).netloc}/api-ws/v1/inference" # WebSocket server address
audio_file = 'asr_example.wav' # Replace with the path to your audio file
# Generate a 32-character random ID
TASK_ID = uuid.uuid4().hex[:32]
task_started = False # Flag indicating whether the task has started
# Send the run-task instruction
def send_run_task(ws):
run_task_message = {
'header': {
'action': 'run-task',
'task_id': TASK_ID,
'streaming': 'duplex'
},
'payload': {
'task_group': 'audio',
'task': 'asr',
'function': 'recognition',
'model': 'qwen-audio-3.1-asr-flash-streaming',
'parameters': {
'sample_rate': 16000,
'format': 'wav'
},
'input': {}
}
}
ws.send(json.dumps(run_task_message))
# Send the finish-task instruction
def send_finish_task(ws):
finish_task_message = {
'header': {
'action': 'finish-task',
'task_id': TASK_ID,
'streaming': 'duplex'
},
'payload': {
'input': {}
}
}
ws.send(json.dumps(finish_task_message))
# Send the audio stream (send one binary chunk every 100ms)
def send_audio_stream(ws):
chunk_size = 3200 # 100ms @ 16kHz 16bit mono
try:
with open(audio_file, 'rb') as f:
while True:
chunk = f.read(chunk_size)
if not chunk:
break
ws.send(chunk, opcode=websocket.ABNF.OPCODE_BINARY)
time.sleep(0.1)
print('Audio stream ended')
send_finish_task(ws)
except Exception as e:
print('Error reading audio file:', e)
ws.close()
# Send the run-task instruction when the connection opens
def on_open(ws):
print('Connected to server')
send_run_task(ws)
# Handle received messages
def on_message(ws, data):
global task_started
message = json.loads(data)
event = message['header']['event']
if event == 'task-started':
print('Task started')
task_started = True
threading.Thread(target=send_audio_stream, args=(ws,), daemon=True).start()
elif event == 'result-generated':
print('Recognition result:', message['payload']['output']['sentence']['text'])
if message['payload'].get('usage'):
print('Audio duration (seconds):', message['payload']['usage']['duration'])
elif event == 'task-finished':
print('Task finished')
ws.close()
elif event == 'task-failed':
print('Task failed:', message['header'].get('error_message'))
ws.close()
else:
print('Unknown event:', event)
# Close the connection if the task-started event is not received
def on_close(ws, close_status_code, close_msg):
if not task_started:
print('Task not started, closing connection')
# Error handling
def on_error(ws, error):
print('WebSocket error:', error)
if __name__ == '__main__':
ws = websocket.WebSocketApp(
url,
header={'Authorization': f'bearer {api_key}'},
on_open=on_open,
on_message=on_message,
on_error=on_error,
on_close=on_close
)
ws.run_forever()
新しい Go サンプルディレクトリでモジュールを初期化し、依存関係をインストールします。
Copy
go mod init asr-example
go get github.com/gorilla/websocket@v1.5.3 github.com/google/uuid@v1.6.0
Copy
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
neturl "net/url"
"os"
"time"
"github.com/google/uuid"
"github.com/gorilla/websocket"
)
const (
audioFile = "asr_example.wav" // Replace with the path to your audio file
)
var dialer = websocket.DefaultDialer
func main() {
apiKey := os.Getenv("DASHSCOPE_API_KEY")
// Connect to the WebSocket service
conn, err := connectWebSocket(apiKey)
if err != nil {
log.Fatal("Failed to connect to WebSocket: ", err)
}
defer closeConnection(conn)
// Start a goroutine to receive results
taskStarted := make(chan bool)
taskDone := make(chan bool)
startResultReceiver(conn, taskStarted, taskDone)
// Send the run-task instruction
taskID, err := sendRunTaskCmd(conn)
if err != nil {
log.Fatal("Failed to send the run-task instruction: ", err)
}
// Wait for the task-started event
waitForTaskStarted(taskStarted)
// Send the audio file stream to be recognized
if err := sendAudioData(conn); err != nil {
log.Fatal("Failed to send audio: ", err)
}
// Send the finish-task instruction
if err := sendFinishTaskCmd(conn, taskID); err != nil {
log.Fatal("Failed to send the finish-task instruction: ", err)
}
// Wait for the task to finish or fail
<-taskDone
}
// Define structs to represent the JSON data
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 Output struct {
Sentence struct {
BeginTime int64 `json:"begin_time"`
EndTime *int64 `json:"end_time"`
Text string `json:"text"`
Words []struct {
BeginTime int64 `json:"begin_time"`
EndTime *int64 `json:"end_time"`
Text string `json:"text"`
Punctuation string `json:"punctuation"`
} `json:"words"`
} `json:"sentence"`
}
type Payload struct {
TaskGroup string `json:"task_group"`
Task string `json:"task"`
Function string `json:"function"`
Model string `json:"model"`
Parameters Params `json:"parameters"`
Input Input `json:"input"`
Output Output `json:"output,omitempty"`
Usage *struct {
Duration int `json:"duration"`
} `json:"usage,omitempty"`
}
type Params struct {
Format string `json:"format"`
SampleRate int `json:"sample_rate"`
DisfluencyRemovalEnabled bool `json:"disfluency_removal_enabled"`
}
type Input struct {
}
type Event struct {
Header Header `json:"header"`
Payload Payload `json:"payload"`
}
// Connect to the WebSocket service
func connectWebSocket(apiKey string) (*websocket.Conn, error) {
base, err := neturl.Parse(os.Getenv("DASHSCOPE_BASE_URL"))
if err != nil {
return nil, err
}
wsURL := "wss://" + base.Host + "/api-ws/v1/inference"
header := make(http.Header)
header.Add("Authorization", fmt.Sprintf("bearer %s", apiKey))
conn, _, err := dialer.Dial(wsURL, header)
return conn, err
}
// Start a goroutine to receive WebSocket messages asynchronously
func startResultReceiver(conn *websocket.Conn, taskStarted chan<- bool, taskDone chan<- bool) {
go func() {
for {
_, message, err := conn.ReadMessage()
if err != nil {
log.Println("Failed to parse server message: ", err)
return
}
var event Event
err = json.Unmarshal(message, &event)
if err != nil {
log.Println("Failed to parse event: ", err)
continue
}
if handleEvent(conn, event, taskStarted, taskDone) {
return
}
}
}()
}
// Send the run-task instruction
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
}
// Generate the run-task instruction
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: "asr",
Function: "recognition",
Model: "qwen-audio-3.1-asr-flash-streaming",
Parameters: Params{
Format: "wav",
SampleRate: 16000,
},
Input: Input{},
},
}
runTaskCmdJSON, err := json.Marshal(runTaskCmd)
return string(runTaskCmdJSON), taskID, err
}
// Wait for the task-started event
func waitForTaskStarted(taskStarted chan bool) {
select {
case <-taskStarted:
fmt.Println("Task started successfully")
case <-time.After(10 * time.Second):
log.Fatal("Timed out waiting for task-started; failed to start the task")
}
}
// Send audio data
func sendAudioData(conn *websocket.Conn) error {
file, err := os.Open(audioFile)
if err != nil {
return err
}
defer file.Close()
buf := make([]byte, 3200)
for {
n, err := file.Read(buf)
if n == 0 {
break
}
if err != nil && err != io.EOF {
return err
}
err = conn.WriteMessage(websocket.BinaryMessage, buf[:n])
if err != nil {
return err
}
time.Sleep(100 * time.Millisecond)
}
return nil
}
// Send the finish-task instruction
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
}
// Generate the finish-task instruction
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
}
// Handle events
func handleEvent(conn *websocket.Conn, event Event, taskStarted chan<- bool, taskDone chan<- bool) bool {
switch event.Header.Event {
case "task-started":
fmt.Println("Received the task-started event")
taskStarted <- true
case "result-generated":
if event.Payload.Output.Sentence.Text != "" {
fmt.Println("Recognition result: ", event.Payload.Output.Sentence.Text)
}
if event.Payload.Usage != nil {
fmt.Println("Audio duration (seconds): ", event.Payload.Usage.Duration)
}
case "task-finished":
fmt.Println("Task finished")
taskDone <- true
return true
case "task-failed":
handleTaskFailed(event, conn)
taskDone <- true
return true
default:
log.Printf("Unexpected event: %v", event)
}
return false
}
// Handle the task-failed event
func handleTaskFailed(event Event, conn *websocket.Conn) {
if event.Header.ErrorMessage != "" {
log.Fatalf("Task failed: %s", event.Header.ErrorMessage)
} else {
log.Fatal("The task failed for an unknown reason")
}
}
// Close the connection
func closeConnection(conn *websocket.Conn) {
if conn != nil {
conn.Close()
}
}
必要な依存関係をインストールします:サンプルコードは以下の通りです:コードを
Copy
npm install ws@8 uuid@9
Copy
const fs = require('fs');
const WebSocket = require('ws');
const { v4: uuidv4 } = require('uuid'); // Used to generate a UUID
const apiKey = process.env.DASHSCOPE_API_KEY;
const url = `wss://${new URL(process.env.DASHSCOPE_BASE_URL).host}/api-ws/v1/inference`; // WebSocket server address
const audioFile = 'asr_example.wav'; // Replace with the path to your audio file
// Generate a 32-character random ID
const TASK_ID = uuidv4().replace(/-/g, '').slice(0, 32);
// Create the WebSocket client
const ws = new WebSocket(url, {
headers: {
Authorization: `bearer ${apiKey}`
}
});
let taskStarted = false; // Flag indicating whether the task has started
// Send the run-task instruction when the connection opens
ws.on('open', () => {
console.log('Connected to server');
sendRunTask();
});
// Handle received messages
ws.on('message', (data) => {
const message = JSON.parse(data);
switch (message.header.event) {
case 'task-started':
console.log('Task started');
taskStarted = true;
sendAudioStream();
break;
case 'result-generated':
console.log('Recognition result:', message.payload.output.sentence.text);
if (message.payload.usage) {
console.log('Audio duration (seconds):', message.payload.usage.duration);
}
break;
case 'task-finished':
console.log('Task finished');
ws.close();
break;
case 'task-failed':
console.error('Task failed:', message.header.error_message);
ws.close();
break;
default:
console.log('Unknown event:', message.header.event);
}
});
// Close the connection if the task-started event is not received
ws.on('close', () => {
if (!taskStarted) {
console.error('Task not started, closing connection');
}
});
// Send the run-task instruction
function sendRunTask() {
const runTaskMessage = {
header: {
action: 'run-task',
task_id: TASK_ID,
streaming: 'duplex'
},
payload: {
task_group: 'audio',
task: 'asr',
function: 'recognition',
model: 'qwen-audio-3.1-asr-flash-streaming',
parameters: {
sample_rate: 16000,
format: 'wav'
},
input: {}
}
};
ws.send(JSON.stringify(runTaskMessage));
}
// Send the audio stream
function sendAudioStream() {
const audioStream = fs.createReadStream(audioFile);
let chunkCount = 0;
function sendNextChunk() {
const chunk = audioStream.read();
if (chunk) {
ws.send(chunk);
chunkCount++;
setTimeout(sendNextChunk, 100); // Send once every 100ms
}
}
audioStream.on('readable', () => {
sendNextChunk();
});
audioStream.on('end', () => {
console.log('Audio stream ended');
sendFinishTask();
});
audioStream.on('error', (err) => {
console.error('Error reading audio file:', err);
ws.close();
});
}
// Send the finish-task instruction
function sendFinishTask() {
const finishTaskMessage = {
header: {
action: 'finish-task',
task_id: TASK_ID,
streaming: 'duplex'
},
payload: {
input: {}
}
};
ws.send(JSON.stringify(finishTaskMessage));
}
// Error handling
ws.on('error', (error) => {
console.error('WebSocket error:', error);
});
asr_example.cjs に保存し、node asr_example.cjs を実行します。コードを
Program.cs に保存し、.NET コンソールプロジェクトで実行します。サンプルコードは以下の通りです:Copy
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
class Program {
private static ClientWebSocket _webSocket = new ClientWebSocket();
private static CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private static bool _taskStartedReceived = false;
private static bool _taskFinishedReceived = false;
private static readonly string ApiKey = Environment.GetEnvironmentVariable("DASHSCOPE_API_KEY") ?? throw new InvalidOperationException("DASHSCOPE_API_KEY environment variable is not set.");
private static readonly string WebSocketUrl = "wss://" + new Uri(Environment.GetEnvironmentVariable("DASHSCOPE_BASE_URL")!).Authority + "/api-ws/v1/inference";
// Replace with the path to your audio file
private const string AudioFilePath = "asr_example.wav";
static async Task Main(string[] args) {
// Establish the WebSocket connection and configure headers for authentication
_webSocket.Options.SetRequestHeader("Authorization", $"bearer {ApiKey}");
await _webSocket.ConnectAsync(new Uri(WebSocketUrl), _cancellationTokenSource.Token);
// Start a thread to receive WebSocket messages asynchronously
var receiveTask = ReceiveMessagesAsync();
// Send the run-task instruction
string _taskId = Guid.NewGuid().ToString("N"); // Generate a 32-character random ID
var runTaskJson = GenerateRunTaskJson(_taskId);
await SendAsync(runTaskJson);
// Wait for the task-started event
while (!_taskStartedReceived) {
await Task.Delay(100, _cancellationTokenSource.Token);
}
// Read the local file and send the audio stream to be recognized to the server
await SendAudioStreamAsync(AudioFilePath);
// Send the finish-task instruction to end the task
var finishTaskJson = GenerateFinishTaskJson(_taskId);
await SendAsync(finishTaskJson);
// Wait for the task-finished event
while (!_taskFinishedReceived && !_cancellationTokenSource.IsCancellationRequested) {
try {
await Task.Delay(100, _cancellationTokenSource.Token);
} catch (OperationCanceledException) {
// The task has been canceled, exit the loop
break;
}
}
// Close the connection
if (!_cancellationTokenSource.IsCancellationRequested) {
await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", _cancellationTokenSource.Token);
}
_cancellationTokenSource.Cancel();
try {
await receiveTask;
} catch (OperationCanceledException) {
// Ignore the operation canceled exception
}
}
private static async Task ReceiveMessagesAsync() {
try {
while (_webSocket.State == WebSocketState.Open && !_cancellationTokenSource.IsCancellationRequested) {
var message = await ReceiveMessageAsync(_cancellationTokenSource.Token);
if (message != null) {
var eventValue = message["header"]?["event"]?.GetValue<string>();
switch (eventValue) {
case "task-started":
Console.WriteLine("Task started successfully");
_taskStartedReceived = true;
break;
case "result-generated":
Console.WriteLine($"Recognition result: {message["payload"]?["output"]?["sentence"]?["text"]?.GetValue<string>()}");
if (message["payload"]?["usage"] != null && message["payload"]?["usage"]?["duration"] != null) {
Console.WriteLine($"Audio duration (seconds): {message["payload"]?["usage"]?["duration"]?.GetValue<int>()}");
}
break;
case "task-finished":
Console.WriteLine("Task finished");
_taskFinishedReceived = true;
_cancellationTokenSource.Cancel();
break;
case "task-failed":
Console.WriteLine($"Task failed: {message["header"]?["error_message"]?.GetValue<string>()}");
_cancellationTokenSource.Cancel();
break;
}
}
}
} catch (OperationCanceledException) {
// Ignore the operation canceled exception
}
}
private static async Task<JsonNode?> ReceiveMessageAsync(CancellationToken cancellationToken) {
var buffer = new byte[1024 * 4];
var segment = new ArraySegment<byte>(buffer);
var sb = new StringBuilder();
WebSocketReceiveResult result;
// Receive and concatenate fragments until EndOfMessage to avoid JSON parsing failures on long messages
do {
result = await _webSocket.ReceiveAsync(segment, cancellationToken);
if (result.MessageType == WebSocketMessageType.Close) {
await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", cancellationToken);
return null;
}
sb.Append(Encoding.UTF8.GetString(buffer, 0, result.Count));
} while (!result.EndOfMessage);
return JsonNode.Parse(sb.ToString());
}
private static async Task SendAsync(string message) {
var buffer = Encoding.UTF8.GetBytes(message);
var segment = new ArraySegment<byte>(buffer);
await _webSocket.SendAsync(segment, WebSocketMessageType.Text, true, _cancellationTokenSource.Token);
}
private static async Task SendAudioStreamAsync(string filePath) {
using (var audioStream = File.OpenRead(filePath)) {
var buffer = new byte[3200]; // Send 100ms of audio data each time
int bytesRead;
while ((bytesRead = await audioStream.ReadAsync(buffer, 0, buffer.Length)) > 0) {
var segment = new ArraySegment<byte>(buffer, 0, bytesRead);
await _webSocket.SendAsync(segment, WebSocketMessageType.Binary, true, _cancellationTokenSource.Token);
await Task.Delay(100); // 100ms interval
}
}
}
private static string GenerateRunTaskJson(string taskId) {
var runTask = new JsonObject {
["header"] = new JsonObject {
["action"] = "run-task",
["task_id"] = taskId,
["streaming"] = "duplex"
},
["payload"] = new JsonObject {
["task_group"] = "audio",
["task"] = "asr",
["function"] = "recognition",
["model"] = "qwen-audio-3.1-asr-flash-streaming",
["parameters"] = new JsonObject {
["format"] = "wav",
["sample_rate"] = 16000,
},
["input"] = new JsonObject()
}
};
return JsonSerializer.Serialize(runTask);
}
private static string GenerateFinishTaskJson(string taskId) {
var finishTask = new JsonObject {
["header"] = new JsonObject {
["action"] = "finish-task",
["task_id"] = taskId,
["streaming"] = "duplex"
},
["payload"] = new JsonObject {
["input"] = new JsonObject()
}
};
return JsonSerializer.Serialize(finishTask);
}
}
依存関係の設定を index.php の内容は以下の通りです:
composer.json に保存し、composer install を実行してから PHP の例を実行します。サンプルプロジェクトのディレクトリ構造は以下の通りです:my-php-project/├── composer.json├── vendor/└── index.phpcomposer.json の内容は以下の通りです。必要に応じて依存関係のバージョンを調整してください:Copy
{
"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/"
}
}
}
Copy
<?php
require __DIR__ . '/vendor/autoload.php';
use Ratchet\Client\Connector;
use React\EventLoop\Loop;
use React\Socket\Connector as SocketConnector;
use Ratchet\rfc6455\Messaging\Frame;
$api_key = getenv("DASHSCOPE_API_KEY");
$websocket_url = 'wss://' . parse_url(getenv('DASHSCOPE_BASE_URL'), PHP_URL_HOST) . '/api-ws/v1/inference';
$audio_file_path = 'asr_example.wav'; // Replace with the path to your audio file
$loop = Loop::get();
// Create a custom connector
$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
];
$connector($websocket_url, [], $headers)->then(function ($conn) use ($loop, $audio_file_path) {
echo "Connected to WebSocket server\n";
// Start a thread to receive WebSocket messages asynchronously
$taskId = generateTaskId();
$conn->on('message', function($msg) use ($conn, $loop, $audio_file_path, $taskId) {
$response = json_decode($msg, true);
if (isset($response['header']['event'])) {
handleEvent($conn, $response, $loop, $audio_file_path, $taskId);
} else {
echo "Unknown message format\n";
}
});
// Listen for the connection close
$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";
}
});
// Generate the task ID
// Send the run-task instruction
sendRunTaskMessage($conn, $taskId);
}, function ($e) {
echo "Unable to connect: {$e->getMessage()}\n";
});
$loop->run();
/**
* Generate the task ID
* @return string
*/
function generateTaskId(): string {
return bin2hex(random_bytes(16));
}
/**
* Send the run-task instruction
* @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" => "asr",
"function" => "recognition",
"model" => "qwen-audio-3.1-asr-flash-streaming",
"parameters" => [
"format" => "wav",
"sample_rate" => 16000
],
"input" => new stdClass()
]
]);
echo "Preparing to send the run-task instruction: " . $runTaskMessage . "\n";
$conn->send($runTaskMessage);
echo "run-task instruction sent\n";
}
/**
* Read the audio file
* @param string $filePath
* @return bool|string
*/
function readAudioFile(string $filePath) {
$voiceData = file_get_contents($filePath);
if ($voiceData === false) {
echo "Unable to read the audio file\n";
}
return $voiceData;
}
/**
* Split the audio data
* @param string $data
* @param int $chunkSize
* @return array
*/
function splitAudioData(string $data, int $chunkSize): array {
return str_split($data, $chunkSize);
}
/**
* Send the finish-task instruction
* @param $conn
* @param $taskId
*/
function sendFinishTaskMessage($conn, $taskId) {
$finishTaskMessage = json_encode([
"header" => [
"action" => "finish-task",
"task_id" => $taskId,
"streaming" => "duplex"
],
"payload" => [
"input" => new stdClass()
]
]);
echo "Preparing to send the finish-task instruction: " . $finishTaskMessage . "\n";
$conn->send($finishTaskMessage);
echo "finish-task instruction sent\n";
}
/**
* Handle events
* @param $conn
* @param $response
* @param $loop
* @param $audio_file_path
*/
function handleEvent($conn, $response, $loop, $audio_file_path, $taskId) {
static $chunks;
static $allChunksSent = false;
switch ($response['header']['event']) {
case 'task-started':
echo "Task started, sending audio data...\n";
// Read the audio file
$voiceData = readAudioFile($audio_file_path);
if ($voiceData === false) {
echo "Unable to read the audio file\n";
$conn->close();
return;
}
// Split the audio data
$chunks = splitAudioData($voiceData, 3200);
// Define the send function
$sendChunk = function() use ($conn, &$chunks, $loop, &$sendChunk, &$allChunksSent, $taskId) {
if (!empty($chunks)) {
$chunk = array_shift($chunks);
$binaryMsg = new Frame($chunk, true, Frame::OP_BINARY);
$conn->send($binaryMsg);
// Send the next chunk after 100ms
$loop->addTimer(0.1, $sendChunk);
} else {
echo "All data chunks sent\n";
$allChunksSent = true;
// Send the finish-task instruction
sendFinishTaskMessage($conn, $taskId);
}
};
// Start sending audio data
$sendChunk();
break;
case 'result-generated':
$result = $response['payload']['output']['sentence'];
echo "Recognition result: " . $result['text'] . "\n";
if (isset($response['payload']['usage']['duration'])) {
echo "Audio duration (seconds): " . $response['payload']['usage']['duration'] . "\n";
}
break;
case 'task-finished':
echo "Task finished\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 all data has been sent and the task has finished, close the connection
if ($allChunksSent && $response['header']['event'] == 'task-finished') {
// Wait 1 second to ensure all data has been transmitted
$loop->addTimer(1, function() use ($conn) {
$conn->close();
echo "Client closed the connection\n";
});
}
}