Skip to main content

Real-time audio transcription interaction protocol for meeting minutes (WebSocket)

This topic describes how to use WebSocket for real-time transcription of meeting minutes.

This topic describes the WebSocket API for real-time transcription of meeting minutes. The WebSocket protocol is the preferred connection method because it has low latency and is resource-efficient. WebSocket is a network protocol that supports full-duplex communication. After a single handshake, a client and a server establish a persistent connection, allowing both parties to send data to each other. This provides significant advantages in real-time performance and efficiency. Before you start, you should understand the basic principles and technical details of WebSocket.

Prerequisites

Activate the service and obtain and configure an API key. To prevent security risks from code leaks, configure the API key as an environment variable instead of hard coding it in your code.
To grant temporary access permissions to third-party applications or users, or want to strictly control risky operations such as accessing or deleting sensitive data, you can use a temporary authentication token.Compared to a long-term API key, a temporary authentication token is valid for only a short period (60 seconds) and offers higher security. It is suitable for temporary call scenarios and can effectively reduce the risk of API key leaks.Usage: In your code, replace the API key used for authentication with the obtained temporary authentication token.

Overall call flow

For more information, see Real-time meeting.

Call time series chart

image
You must follow the sequence shown in the time series chart. Otherwise, the call may fail.

Establish a WebSocket connection

This corresponds to step 1 in the time series chart. For common programming languages, many ready-to-use WebSocket libraries and examples are available, such as:
  • Go: gorilla/websocket
  • PHP: Ratchet
  • Node.js: ws

Connection request header

Authorization: Bearer {api-key} // Replace {api-key} with your actual API key.
Upgrade: websocket
Connection: Upgrade

WebSocket access URL

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

Send instructions to the server

You can send instructions to the server to start and stop transcription. There are two types of instructions: run-task and finish-task. Both must be sent as JSON data in a Text Frame. The protocol for these instructions is as follows:

run-task instruction

This corresponds to step 2 in the time series chart. It notifies the server to start a transcription task. The protocol fields are as follows:

Field

Type

Description

header

Object

header.action

String

Set to run-task.

header.task_id

String

A custom 16-character random string used for troubleshooting. The subsequent finish-task must use the same task_id.

header.streaming

String

Set to duplex.

payload

Object

payload.model

String

Set to tingwu-meeting-realtime.

payload.task_group

String

Set to aigc.

payload.task

String

Set to multimodal-generation.

payload.function

String

Set to generation.

payload.input

Object

payload.input.appId

String

The ID of the application published in the meeting minutes console. You can obtain it from the console.

payload.input.directive

String

Set to start.

payload.input.dataId

String

The dataId obtained from CreateTask.

The following is an example of a run-task instruction:
{
  "header": {
    "action": "run-task",
    "task_id": "f2E3zvK*******wp",
    "streaming": "duplex"
  },
  "payload": {
    "model": "tingwu-meeting-realtime",
    "task_group": "aigc",
    "task": "multimodal-generation",
    "function": "generation",
    "input": {
      "appId": "tw_YrN*******Cw",
      "dataId": "uTx*******bN",
      "directive": "start"
    }
  }
}

finish-task

This corresponds to step 6 in the time series chart. It notifies the server that the audio has been completely sent. The protocol fields are as follows:

Field

Type

Description

header

Object

header.action

String

Set to finish-task.

header.task_id

String

Enter the task_id that you specified in the run-task command.

header.streaming

String

Set to duplex.

payload

Object

payload.model

String

Set to tingwu-meeting-realtime.

payload.task_group

String

Set to aigc.

payload.task

String

Set to multimodal-generation.

payload.function

String

Set to generation.

payload.input

Object

payload.input.directive

String

Set to stop.

The following is an example of a finish-task instruction:
{
  "header": {
    "action": "finish-task",
    "task_id": "f2E3zvK*******wp",
    "streaming": "duplex"
  },
  "payload": {
    "model": "tingwu-meeting-realtime",
    "task_group": "aigc",
    "task": "multimodal-generation",
    "function": "generation",
    "input": {
      "directive": "stop"
    }
  }
}

Send audio to the server

Convert the raw audio directly into a binary stream without any additional processing. Note the following:
  • The audio sampling rate for speech recognition must be 8000 Hz or 16000 Hz. It must be consistent with the parameter passed when calling CreateTask.
  • The audio coding format must be consistent with the parameter passed when calling CreateTask.
  • Supported audio formats: PCM, Opus, AAC, Speex, and MP3.

Receive events from the server

After you send an instruction or audio, the server sends different types of events. Each event represents a different processing stage. You must handle each event according to the sequence shown in the time series chart. There are five types of events: speech-listen, recognize-result, heartbeat, speech-end, and task-failed.

speech-listen event

This corresponds to step 3 in the time series chart. The server returns the speech-listen event after it receives the run-task instruction. This event indicates that the server has received your transcription instruction, completed initialization, and is ready to receive audio. The protocol fields are as follows:

Field

Type

Description

header

Object

header.event

String

Set to result-generated.

header.task_id

String

The task_id you specified in the run-task instruction.

payload

Object

payload.output

Object

payload.output.action

String

Set to speech-listen.

payload.output.dataId

String

The dataId you passed.

The following is an example of a speech-listen event:
{
  "header": {
    "event": "result-generated",
    "task_id": "f2E3zvK*******wp"
  },
  "payload": {
    "output": {
      "action": "speech-listen",
      "dataId": "Adb*******uY"
    }
  }
}

recognize-result event

This corresponds to step 4 in the time series chart. The server returns a recognize-result event while you are sending audio. This event can also be returned after you send the finish-task instruction. It contains the current recognition and translation results from the server. The protocol fields are as follows:

Field

Type

Description

header

Object

header.event

String

Set to result-generated.

header.task_id

String

The task_id you specified in the run-task instruction.

payload

Object

payload.output

Object

payload.output.action

String

Set to recognize-result.

payload.output.transcription

Object

The original transcription result.

payload.output.transcription.sentenceId

Integer

The sentence ordinal number.

payload.output.transcription.time

Integer

The duration of the audio that has been processed.

payload.output.transcription.text

String

The recognized text.

payload.output.transcription.words

Array[Word]

Character timestamp information.

payload.output.transcription.sentenceEnd

Boolean

true: The current text forms a complete sentence.

false: The current text does not form a complete sentence. The recognition result may be updated.

payload.output.transcription.stashResult

Object

The staging result of speech recognition. It contains information about the next sentence that is not yet segmented. You can concatenate the stashResult with the text result for subsequent processing.

payload.output.transcription.stashResult.sentenceId

Integer

The sentence ID of the next sentence.

payload.output.transcription.stashResult.text

String

The ASR text of the stash result.

payload.output.transcription.stashResult.words

Array[Word]

The word information of the stash result.

payload.output.translations

Object

The translation result.

payload.output.translations[targetLang].sourceLang

String

The source language.

payload.output.translations[targetLang].targetLang

String

The target language.

payload.output.translations[targetLang].translateResult

Array[Object]

The translated sentence result.

payload.output.translations[targetLang].translateResult[i].sentenceId

Integer

The translated sentence number. It increments from 0.

payload.output.translations[targetLang].translateResult[i].text

String

The sentence translation result.

payload.output.translations[targetLang].translateResult[i].beginTime

Integer

The start time of the translated sentence, in milliseconds. This is returned when translating a SentenceEnd recognition result.

payload.output.translations[targetLang].translateResult[i].endTime

Integer

The end time of the translated sentence, in milliseconds. This is returned when translating a SentenceEnd recognition result.

payload.output.translations[targetLang].translateResult[i].partial

Boolean

If true, this is the translation result for the stashed recognition content.

The protocol for the Word type is as follows:

Field

Type

Description

beginTime

Integer

The start time of the current word in the audio.

endTime

Integer

The end time of the current word in the audio.

text

String

The content of the current word.

Note that a single result will contain either a recognition result or a translation result, but not both at the same time. The following are examples of a recognize-result event:
Sentence start detected
{
  "header": {
    "event":"result-generated",
    "task_id": "f2E3zvK*******w"
  },
  "payload": {
    "output": {
      "action": "recognize-result",
      "transcription": {
        "sentenceId": 1,
        "time": 10000
      }
    }
  }
}
Mid-sentence detection
{
  "header": {
    "event":"result-generated",
    "task_id": "f2E3zvK*******w"
  },
  "payload": {
    "output": {
      "action": "recognize-result",
      "transcription": {
        "sentenceId": 0,
        "sentenceEnd": false,
        "time": 2065,
        "text": "This is a sentence for testing",
        "words": [
          {
            "beginTime": 100,
            "endTime": 427,
            "text": "This"
          },
          {
            "beginTime": 427,
            "endTime": 755,
            "text": "is a"
          },
          {
            "beginTime": 755,
            "endTime": 1082,
            "text": "sentence"
          },
          {
            "beginTime": 1082,
            "endTime": 1410,
            "text": "used for"
          },
          {
            "beginTime": 1410,
            "endTime": 1737,
            "text": "testing"
          },
          {
            "beginTime": 1737,
            "endTime": 2065,
            "text": "of"
          }
        ]
      }
    }
  }
}
Sentence end detected
{
  "header": {
    "event":"result-generated",
    "task_id": "f2E3zvK*******w"
  },
  "payload": {
    "output": {
      "action": "recognize-result",
      "transcription": {
        "sentenceId": 0,
        "time": 2720,
        "sentenceEnd": true,
        "text": "This is a sentence for testing text.",
        "words": [
          {
            "beginTime": 100,
            "endTime": 427,
            "text": "This"
          },
          {
            "beginTime": 427,
            "endTime": 755,
            "text": "is a"
          },
          {
            "beginTime": 755,
            "endTime": 1082,
            "text": "sentence"
          },
          {
            "beginTime": 1082,
            "endTime": 1410,
            "text": "used for"
          },
          {
            "beginTime": 1410,
            "endTime": 1737,
            "text": "testing"
          },
          {
            "beginTime": 1737,
            "endTime": 2065,
            "text": "of"
          },
          {
            "beginTime": 2065,
            "endTime": 2392,
            "text": "text"
          },
          {
            "beginTime": 2392,
            "endTime": 2720,
            "text": "."
          }
        ],
        "stashResult": {
          "sentenceId": 1,
          "words": [
            {
              "beginTime": 2920,
              "text": "will",
              "endTime": 3030
            },
            {
              "beginTime": 3030,
              "text": "rain",
              "endTime": 3230
            }
          ],
          "text": "It will rain"
        }
      }
    }
  }
}
Translation result
{
  "header": {
    "event":"result-generated",
    "task_id": "f2E3zvK*******w"
  },
  "payload": {
    "output": {
      "action": "recognize-result",
      "translations": {
        "en": {
          "sourceLang":"cn",
          "targetLang":"en",
          "translateResult":[
            {
              "text":"At that time.",
              "sentenceId":110,
              "beginTime":123000,
              "endTime":125000
            },
            {
              "text":"I am",
              "sentenceId":111,
              "partial":true
            }
          ]
        }
      }
    }
  }
}

Heartbeat event

This corresponds to step 5 in the time series chart. If you send silent audio for an extended period, the server sends a heartbeat event to keep the connection alive. The heartbeat event is sent 30 seconds after the previous event. The client does not need to reply to the heartbeat event. The protocol fields are as follows:

Field

Type

Description

header

Object

header.event

String

Set to result-generated.

header.task_id

String

The task_id you specified in the run-task instruction.

payload

Object

payload.output

Object

payload.output.action

String

Set to ping.

The following is an example of a heartbeat event:
{
  "header": {
    "event": "result-generated",
    "task_id": "f2E3zvK*******wp"
  },
  "payload": {
    "output": {
      "action": "ping"
    }
  }
}

speech-end event

This corresponds to step 7 in the time series chart. The speech-end event indicates that all results for the current real-time transcription have been sent. You can then close the WebSocket connection. The protocol fields are as follows:

Field

Type

Description

header

Object

header.event

String

Set to result-generated.

header.task_id

String

The task_id you specified in the run-task instruction.

payload

Object

payload.output

Object

payload.output.action

String

Set to speech-end.

The following is an example of a speech-end event:
{
  "header": {
    "event": "result-generated",
    "task_id": "f2E3zvK*******wp"
  },
  "payload": {
    "output": {
      "action": "speech-end"
    }
  }
}

task-failed event

If a task fails during processing because of incorrect client parameters or an internal server error, the server returns a task-failed event and then terminates the WebSocket connection. The protocol fields are as follows:

Field

Type

Description

header

Object

header.event

String

Set to result-generated.

header.task_id

String

The task_id you specified in the run-task instruction.

payload

Object

payload.output

Object

payload.output.action

String

Set to task-failed.

payload.output.errorCode

String

The error code.

payload.output.errorMessage

String

The error message.

The following is an example of a task-failed event:
{
  "header": {
    "event": "result-generated",
    "task_id": "f2E3zvK*******wp"
  },
  "payload": {
    "output": {
      "action": "task-failed",
      "errorCode": "Agent.FrameSequenceIllegal",
      "errorMessage": "Agent Websocket Frame Sequence Illegal."
    }
  }
}
For more information about specific error codes and their meanings, see Error codes.

Pause and resume a meeting

During a WebSocket session, you can pause the meeting at any time by sending a finish-task instruction. If the client does not send any text instructions or binary audio streams for 10 seconds, the connection is automatically terminated. A single meeting dataId is valid for 24 hours. During this period, you can resume the specified meeting at any time by re-establishing a WebSocket connection and providing the same meeting dataId.

Code examples

package nls.alibaba;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.codec.binary.Hex;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map;

import static java.lang.Thread.sleep;

public class TingWuMeetingApiUsage extends WebSocketClient {

  private final byte[] audioData;

  private final String taskId;

  private final ObjectMapper objectMapper = new ObjectMapper();

  private boolean audioSent = false;

  private static final int CHUNK_SIZE = 3200;

  private static final long SEND_INTERVAL_MS = 100; // Simulate the stream sending interval

  private static final String apiKey = "your-api-key";

  public TingWuMeetingApiUsage(URI serverUri, String audioFilePath) throws IOException {
    super(serverUri, createHeaders());
    this.audioData = Files.readAllBytes(new File(audioFilePath).toPath());

    // Generate a random 16-character string
    SecureRandom random = new SecureRandom();
    byte[] taskIdBytes = new byte[8];
    random.nextBytes(taskIdBytes);
    this.taskId = Hex.encodeHexString(taskIdBytes);
  }

  private static Map<String, String> createHeaders() {
    Map<String, String> headers = new HashMap<>();
    headers.put("Authorization", "Bearer " + apiKey);
    headers.put("Upgrade", "websocket");
    headers.put("Connection", "Upgrade");
    return headers;
  }

  @Override
  public void onOpen(ServerHandshake serverHandshake) {
    System.out.println("WebSocket connected.");
    System.out.println("Task ID: " + taskId);

    // Construct the run-task message
    String runTaskMessage = "{\n" +
      "  \"header\": {\n" +
      "    \"action\": \"run-task\",\n" +
      "    \"task_id\": \"" + taskId + "\",\n" +
      "    \"streaming\": \"duplex\"\n" +
      "  },\n" +
      "  \"payload\": {\n" +
      "    \"model\": \"tingwu-meeting-realtime\",\n" +
      "    \"task_group\": \"aigc\",\n" +
      "    \"task\": \"multimodal-generation\",\n" +
      "    \"function\": \"generation\",\n" +
      "    \"input\": {\n" +
      "      \"appId\": \"your-app-id\",\n" +
      "      \"dataId\": \"the-dataid-from-createtask\",\n" +
      "      \"directive\": \"start\"\n" +
      "    }\n" +
      "  }\n" +
      "}";

    System.out.println("Sending run-task: " + runTaskMessage);
    send(runTaskMessage);
  }

  @Override
  public void onMessage(String message) {
    System.out.println("Received text message: " + message);

    try {
      JsonNode root = objectMapper.readTree(message);

            JsonNode header = root.get("header");
            JsonNode payload = root.get("payload");

            if (header != null) {
                String event = header.path("event").asText("");
                // This message can be ignored
                if ("task-started".equals(event)) {
                    return;
                }

                String action = payload.path("output").path("action").asText("");
                switch (action) {
                    case "speech-listen":
                        System.out.println("Received speech-listen event, sending full audio...");
                        sendAudioFile();
                        break;
                    case "task-failed":
                        String errorCode = payload.path("output").path("errorCode").asText("N/A");
                        String errorMsg = payload.path("output").path("errorMessage").asText("N/A");
                        System.out.println("Task failed: " + errorCode + " - " + errorMsg);
                        close();
                        break;
                    case "recognize-result":
                        JsonNode output = payload.get("output");
                        System.out.println("Recognition result: " + (output != null ? output.toString() : "N/A"));
                        break;
                    case "ping":
                        // Keepalive, can be ignored
                        System.out.println("Received Ping action, do nothing...");
                        break;
                    case "speech-end":
                        System.out.println("Speech ended. Closing...");
                        close();
                        break;
                    default:
                        System.out.println("Unknown message received: " + message);
                        break;
                }
            } else {
                System.out.println("Unknown message format (no header): " + message);
            }

        } catch (JsonProcessingException e) {
            System.err.println("Invalid JSON received: " + e.getMessage());
            System.out.println("Raw message: " + message);
        }
    }

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

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

    private void sendAudioFile() {
        if (audioSent) {
            System.out.println("Audio already sent, skipping...");
            return;
        }
        audioSent = true;

        int offset = 0;
        while (offset < audioData.length) {
            int len = Math.min(CHUNK_SIZE, audioData.length - offset);
            byte[] chunk = new byte[len];
            System.arraycopy(audioData, offset, chunk, 0, len);
            try {
                send(chunk);
                offset += len;
                System.out.println("Sent chunk: " + offset + "/" + audioData.length);
                sleep(SEND_INTERVAL_MS);
            } catch (Exception e) {
                System.err.println("Send failed at offset " + offset);
                e.printStackTrace();
                close();
                return;
            }
        }
        System.out.println("Audio sent completely.");
        sendFinishTask();

    }

    // Send the finish-task message
    private void sendFinishTask() {
        String finishTaskMessage = "{\n" +
                "  \"header\": {\n" +
                "    \"action\": \"finish-task\",\n" +
                "    \"task_id\": \"" + taskId + "\",\n" +
                "    \"streaming\": \"duplex\"\n" +
                "  },\n" +
                "  \"payload\": {\n" +
                "    \"model\": \"tingwu-meeting-realtime\",\n" +
                "    \"task_group\": \"aigc\",\n" +
                "    \"task\": \"multimodal-generation\",\n" +
                "    \"function\": \"generation\",\n" +
                "    \"input\": {\n" +
                "      \"directive\": \"stop\"\n" +
                "    }\n" +
                "  }\n" +
                "}";

        System.out.println("Sending finish-task: " + finishTaskMessage);
        send(finishTaskMessage);
    }

    public static void main(String[] args) {
        try {
            URI wsUri = new URI("wss://dashscope.aliyuncs.com/api-ws/v1/inference");
            String audioPath = "path/to/your/audio/file.wav";
            TingWuMeetingApiUsage client = new TingWuMeetingApiUsage(wsUri, audioPath);
            client.connect();
        } catch (Exception e) {
            System.err.println("Initialization error: " + e.getMessage());
            e.printStackTrace();
        }
    }

}

Error codes

Error code

Error message

Description

InvalidParameter

Invalid parameter. For more information, see the official documentation.

The parameter is invalid. Check the parameters that you provided.

Agent.FrameSequenceIllegal

The WebSocket frame sequence is invalid.

The instruction call sequence is invalid.

Agent.InputActionIllegal

The input action is invalid.

The action field in the instruction is invalid.

Agent.InputAppIdIllegal

The input appId is invalid.

The appId field is invalid.

Agent.AppNotPublished

The application is not published.

The application with the specified ID has not been published.

Agent.InputInvalidDataId

The input dataId is invalid.

The dataId is invalid.

Agent.CustomTaskIdInvalid

The custom task ID must be 16 characters long.

The length of the taskId field is invalid. It must be a 16-character string.

BIL.ServiceNotActivate

The user has not activated the service.

You have not activated the Tingwu Agent service.

BIL.UserArrears

The user's account has an overdue payment.

Your account has an overdue payment.

Agent.AppInfoNotExist

The application information does not exist.

The application information for the specified ID does not exist. Save and publish the application configuration in the console first.

ServerError

An internal server error occurred.

Internal server error.

If you encounter an error code that is not on this list, see Error messages for troubleshooting information.
Agent Development
Agent Context
Agent Governance & Evolution
Store
Application Marketplace