Skip to main content
Melhores Práticas

Transcrever fala em tempo real com fun-asr-realtime via AOQ

Use o AOQ para se conectar ao fun-asr-realtime, transmitir áudio do microfone e receber resultados de transcrição em tempo real. O código do cliente usa Android Java, e outras plataformas compatíveis com AOQ oferecem as mesmas interfaces.

Visão geral da solução

O fun-asr-realtime transcreve um fluxo de áudio em texto pontuado em tempo real. O SDK do AOQ transporta mídia e eventos por tracks separadas: o cliente envia áudio pela track de Audio e usa a track de Data para enviar eventos de controle e receber eventos de reconhecimento. O modelo utiliza o protocolo de evento Inference, não o protocolo de evento Realtime. Esta solução é adequada para legendas ao vivo, transcrição de reuniões, entrada de voz e assistentes inteligentes. A track de Audio elimina a necessidade de codificar áudio em mensagens de evento, enquanto a track de Data preserva a semântica das tarefas, como run-task, result-generated e finish-task.
  1. O cliente solicita credenciais temporárias de conexão AOQ ao servidor de aplicativos.
  2. O servidor de aplicativos usa uma chave de API para solicitar um token ao Model Studio e retorna os campos de conexão ao cliente.
  3. O cliente estabelece uma conexão AOQ e envia run-task. Após receber task-started, o cliente começa a enviar o áudio do microfone.
  4. O servidor retorna continuamente eventos result-generated. Depois que o cliente envia finish-task, ele aguarda o resultado final e task-finished.

Pré-requisitos

  1. Ative o Alibaba Cloud Model Studio e crie uma chave de API conforme descrito em Obtain and configure an API key. Armazene a chave de API apenas no servidor de aplicativos. Não a inclua no código do cliente nem a confirme em um repositório de código.
  2. Selecione um endpoint AOQ para a região onde sua aplicação está implantada. Para seleção de endpoints, consulte Select a region, deployment scope, and endpoint.
  3. Baixe o AOQ Client SDK mais recente conforme descrito em SDK download. Este tutorial transporta áudio PCM e não requer o plugin opcional Opus.
  4. Configure um servidor de aplicativos conforme descrito em Token authentication e implemente a autenticação proxy no lado do servidor para o protocolo AOQ Inference. Antes de cada nova conexão, o cliente deve obter novas credenciais de conexão do servidor de aplicativos.

Importar o SDK

Escolha as instruções de integração do SDK para sua plataforma de desenvolvimento. A implementação do cliente usa Android Java; outras plataformas seguem o mesmo design de API e fluxo de eventos.
  • Android
  • iOS
  • HarmonyOS
  • Linux (Python)
  1. Coloque AoqClientSdk-release.aar em app/libs e configure a dependência e as ABIs em app/build.gradle:
android {
    defaultConfig {
        minSdk 21
        ndk { abiFilters 'armeabi-v7a', 'arm64-v8a' }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.aar'])
}
  1. Declare as permissões de rede e gravação no AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
  1. Solicite a permissão de tempo de execução RECORD_AUDIO antes do início da gravação. O reconhecimento de fala não requer a permissão CAMERA.

Experimentar a demonstração

Use a demonstração Android do Alibaba Cloud Model Studio para verificar rapidamente a conectividade AOQ. Baixe o APK e configure a chave de API e workspaceId para testar modelos selecionados. Escaneie o QR code a seguir para baixar a demonstração:
QR code for downloading the demo

Fluxo de implementação

  1. O servidor de aplicativos usa a URL de token Inference para obter parâmetros de conexão AOQ para o fun-asr-realtime.
  2. O cliente mapeia a resposta do token para AoqConnectConfig, publica as tracks de Audio e Data e assina a track de Data.
  3. O cliente configura os parâmetros de codificação de áudio com base nos requisitos do negócio e do modelo, inicia a captura do microfone com o envio de áudio desativado e estabelece a conexão AOQ.
  4. Após o estabelecimento da conexão, o cliente envia run-task e ativa o envio pela track de Audio depois de receber task-started.
  5. O cliente processa result-generated em onDataMsg. Para interromper o reconhecimento, ele primeiro desativa o envio pela track de Audio e então envia finish-task.
  6. Depois de receber task-finished, o cliente pode iniciar outra tarefa na mesma conexão com um novo task_id, ou desconectar e destruir o engine.
Sequence diagram for real-time speech recognition with fun-asr-realtime over AOQ

Obter um token no servidor de aplicativos

Defina DASHSCOPE_API_KEY no servidor de aplicativos e envie uma solicitação para o endpoint da região selecionada. clientIp é o endereço IP público real do dispositivo cliente. O campo é opcional, mas recomendamos fornecê-lo para que o service possa selecionar um endpoint de retransmissão apropriado.
curl -X POST \
  "https://{endpoint}/api/v1/webrtc/inference?model=fun-asr-realtime" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${DASHSCOPE_API_KEY}" \
  -H "x-dashscope-rtc-transport: moq" \
  -d "{\"clientIp\": \"${CLIENT_REAL_IP}\"}"
Se o servidor de aplicativos não conseguir obter o endereço IP público real do dispositivo cliente, omita clientIp do corpo da solicitação em vez de passar uma string vazia.
Retorne os seguintes campos de resposta ao cliente. Nunca retorne a chave de API ao cliente. Para todos os campos de solicitação e resposta, consulte Token authentication.

Campo de resposta

Campo do SDK

aoqTokenForClient

AoqConnectConfig.token

sid

AoqConnectConfig.sid

clientRelayCertFingerprint

AoqConnectConfig.certFingerprint

clientRelayEndpoints

AoqConnectConfig.relayEndpoints

extraInfo.workspaceIdHash

AoqConnectConfig.workspaceIdHash

Implementar o cliente Android

Os passos a seguir dividem o código do cliente Android Java de acordo com a sequência real de conexão e tarefas. Cada trecho é retirado do exemplo completo apresentado posteriormente neste tópico.

1. Criar o engine e registrar callbacks

Crie um engine de cliente AOQ e registre callbacks para alterações de status de conexão e eventos da track de Data. Implemente a lógica de callback para sua aplicação e inicie o reconhecimento somente após o estabelecimento da conexão.
AoqClientListener listener = new AoqClientListener() {
    @Override
    public void onConnectionStatusChange(AoqClientEngine.AoqConnectionStatus status) {
        connected = status == AoqClientEngine.AoqConnectionStatus
                .AoqConnectionStatusConnected;
        if (connected) {
            beginRecognition();
        }
    }

    @Override
    public void onDataMsg(AoqClientEngine.AoqDataMsg msg) {
        handleServerEvent(msg);
    }
};

AoqClientEngine.AoqCreateConfig createConfig = new AoqClientEngine.AoqCreateConfig();
createConfig.workDir = context.getFilesDir().getAbsolutePath();
engine = AoqClientEngine.createEngine(context, createConfig, listener);

2. Configurar a codificação de áudio

Configure a codificação de áudio enviada ao modelo. Defina o formato, taxa de amostragem e contagem de canais com base nos requisitos da sua aplicação e do modelo. O código a seguir usa PCM mono de 16 kHz como exemplo. Para valores suportados, consulte os parâmetros de run-task em Client events.
AoqClientEngine.AoqAudioCodecConfig encoder =
        new AoqClientEngine.AoqAudioCodecConfig();
encoder.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
encoder.codecType = AoqClientEngine.AoqEncoderType.AoqEncoderTypeAudioPCM;
encoder.sampleRate = 16000;
encoder.channel = 1;
encoder.bitrate = 24000;
engine.setAudioEncoderConfig(encoder);

3. Configurar a conexão e as tracks de transporte

Configure a conexão AOQ com as credenciais retornadas pelo servidor de aplicativos e selecione as tracks para publicar e assinar com base na sua aplicação. Para reconhecimento de fala em tempo real, este exemplo publica as tracks de Audio e Data e assina a track de Data.
addTrack(connectConfig, true,
        AoqClientEngine.AoqTrackType.AoqTrackTypeAudio);
addTrack(connectConfig, true,
        AoqClientEngine.AoqTrackType.AoqTrackTypeData);
addTrack(connectConfig, false,
        AoqClientEngine.AoqTrackType.AoqTrackTypeData);

4. Iniciar a captura de áudio e estabelecer a conexão

Configure a captura de áudio e estabeleça a conexão AOQ. Escolha captura integrada ou externa, se deve ativar o modo VoIP e a contagem de canais com base na sua aplicação. Mantenha o envio pela track de Audio desativado até receber task-started.
AoqClientEngine.AoqAudioCaptureConfig capture =
        new AoqClientEngine.AoqAudioCaptureConfig();
capture.isExternal = false;
capture.isVoipMode = true;
capture.channel = 1;
engine.startAudioCapture(capture);

// Do not send audio until task-started is received.
engine.enableSendMediaStream(
        AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, false);
engine.connect(connectConfig);

5. Iniciar uma tarefa de reconhecimento

Após estabelecer a conexão, gere um ID de tarefa e envie run-task para iniciar o reconhecimento. Configure model, format, sample_rate e outros parâmetros de tarefa para o modelo e entrada de áudio que você utiliza. Para detalhes, consulte Client events.
taskId = UUID.randomUUID().toString();
JSONObject header = createHeader("run-task");
JSONObject parameters = new JSONObject()
        .put("format", "pcm")
        .put("sample_rate", 16000);
JSONObject payload = new JSONObject()
        .put("task_group", "audio")
        .put("task", "asr")
        .put("function", "recognition")
        .put("model", "fun-asr-realtime")
        .put("parameters", parameters)
        .put("input", new JSONObject());
send(new JSONObject().put("header", header).put("payload", payload));

6. Processar eventos do servidor

Gerencie estados de tarefas, resultados de reconhecimento e erros, e passe os resultados para sua aplicação. Implemente callbacks com base nos requisitos de UI e gerenciamento de estado. Envie áudio apenas após receber task-started e exclua eventos de heartbeat das transcrições exibidas. Para o esquema completo de resposta, consulte Server events.
String eventName = header.optString("event", "");
if ("task-started".equals(eventName)) {
    taskStarted = true;
    engine.enableSendMediaStream(
            AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, true);
} else if ("result-generated".equals(eventName)) {
    JSONObject payload = event.optJSONObject("payload");
    JSONObject output = payload == null ? null : payload.optJSONObject("output");
    JSONObject sentence = output == null ? null : output.optJSONObject("sentence");
    if (sentence != null && !sentence.optBoolean("heartbeat", false)) {
        String text = sentence.optString("text", "");
        if (!text.isEmpty()) {
            resultListener.onResult(
                    text, sentence.optBoolean("sentence_end", false));
        }
    }
} else if ("task-finished".equals(eventName)) {
    resetTaskState();
    resultListener.onTaskFinished();
} else if ("task-failed".equals(eventName)) {
    String message = header.optString("error_message", "Recognition failed");
    resetTaskState();
    resultListener.onError(message);
}

7. Concluir a tarefa de reconhecimento

Quando o usuário terminar a gravação atual, pare de enviar áudio e envie finish-task. Mantenha a conexão aberta até que a transcrição final e task-finished cheguem. Em seguida, você pode iniciar outra tarefa ou liberar a conexão com base no fluxo da sua aplicação. Para o formato do evento, consulte Client events.
engine.enableSendMediaStream(
        AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, false);
JSONObject payload = new JSONObject().put("input", new JSONObject());
send(new JSONObject()
        .put("header", createHeader("finish-task"))
        .put("payload", payload));

8. Desconectar e destruir o engine

Libere a captura de áudio, a conexão AOQ e os recursos do engine quando a página for destruída ou o reconhecimento não for mais necessário. Escolha o ponto de liberação com base no ciclo de vida da sua aplicação e não libere recursos imediatamente após o envio de finish-task.
engine.enableSendMediaStream(
        AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, false);
engine.stopAudioCapture();
engine.disconnect();
AoqClientEngine.destroy();

Exemplo completo

Esta classe Android Java mapeia a resposta JSON do servidor de aplicativos para AoqConnectConfig e combina a lógica anterior de conexão, captura, tarefa e liberação de recursos.
import android.content.Context;

import com.alibaba.aoq.clientsdk.AoqClientEngine;
import com.alibaba.aoq.clientsdk.AoqClientListener;

import org.json.JSONArray;
import org.json.JSONObject;

import java.nio.charset.StandardCharsets;
import java.util.UUID;

public final class AsrClient {
    public interface ResultListener {
        void onResult(String text, boolean sentenceEnd);

        void onTaskFinished();

        void onError(String message);
    }

    private final AoqClientEngine engine;
    private final ResultListener resultListener;
    private String taskId;
    private boolean connected;
    private boolean taskStarted;

    public AsrClient(Context context, AoqClientEngine.AoqConnectConfig connectConfig,
                     ResultListener resultListener) {
        this.resultListener = resultListener;
        AoqClientListener listener = new AoqClientListener() {
            @Override
            public void onConnectionStatusChange(AoqClientEngine.AoqConnectionStatus status) {
                connected = status == AoqClientEngine.AoqConnectionStatus
                        .AoqConnectionStatusConnected;
                if (connected) {
                    beginRecognition();
                }
            }

            @Override
            public void onDataMsg(AoqClientEngine.AoqDataMsg msg) {
                handleServerEvent(msg);
            }
        };

        AoqClientEngine.AoqCreateConfig createConfig = new AoqClientEngine.AoqCreateConfig();
        createConfig.workDir = context.getFilesDir().getAbsolutePath();
        engine = AoqClientEngine.createEngine(context, createConfig, listener);

        configureAudioEncoder();
        configureTracks(connectConfig);
        startAudioCapture();
        engine.connect(connectConfig);
    }

    private void configureAudioEncoder() {
        AoqClientEngine.AoqAudioCodecConfig encoder = new AoqClientEngine.AoqAudioCodecConfig();
        encoder.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
        encoder.codecType = AoqClientEngine.AoqEncoderType.AoqEncoderTypeAudioPCM;
        encoder.sampleRate = 16000;
        encoder.channel = 1;
        encoder.bitrate = 24000;
        engine.setAudioEncoderConfig(encoder);
    }

    private static void configureTracks(AoqClientEngine.AoqConnectConfig connectConfig) {
        addTrack(connectConfig, true, AoqClientEngine.AoqTrackType.AoqTrackTypeAudio);
        addTrack(connectConfig, true, AoqClientEngine.AoqTrackType.AoqTrackTypeData);
        addTrack(connectConfig, false, AoqClientEngine.AoqTrackType.AoqTrackTypeData);
    }

    private void startAudioCapture() {
        AoqClientEngine.AoqAudioCaptureConfig capture =
                new AoqClientEngine.AoqAudioCaptureConfig();
        capture.isExternal = false;
        capture.isVoipMode = true;
        capture.channel = 1;
        engine.startAudioCapture(capture);
        engine.enableSendMediaStream(AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, false);
    }

    /** Starts another recognition task on the existing AOQ connection. */
    public void beginRecognition() {
        if (!connected || taskStarted || taskId != null) {
            return;
        }
        taskId = UUID.randomUUID().toString();
        JSONObject header = createHeader("run-task");
        JSONObject parameters = new JSONObject()
                .put("format", "pcm")
                .put("sample_rate", 16000);
        JSONObject payload = new JSONObject()
                .put("task_group", "audio")
                .put("task", "asr")
                .put("function", "recognition")
                .put("model", "fun-asr-realtime")
                .put("parameters", parameters)
                .put("input", new JSONObject());
        send(new JSONObject().put("header", header).put("payload", payload));
    }

    /** Stops the current task. Wait for task-finished before closing the connection. */
    public void finishRecognition() {
        if (taskId == null) {
            return;
        }
        engine.enableSendMediaStream(AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, false);
        JSONObject payload = new JSONObject().put("input", new JSONObject());
        send(new JSONObject()
                .put("header", createHeader("finish-task"))
                .put("payload", payload));
    }

    public void close() {
        engine.enableSendMediaStream(AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, false);
        engine.stopAudioCapture();
        engine.disconnect();
        AoqClientEngine.destroy();
    }

    private void handleServerEvent(AoqClientEngine.AoqDataMsg msg) {
        if (msg == null || msg.data == null) {
            return;
        }
        JSONObject event = new JSONObject(new String(msg.data, StandardCharsets.UTF_8));
        JSONObject header = event.optJSONObject("header");
        if (header == null) {
            return;
        }

        String eventName = header.optString("event", "");
        if ("task-started".equals(eventName)) {
            taskStarted = true;
            engine.enableSendMediaStream(
                    AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, true);
        } else if ("result-generated".equals(eventName)) {
            handleRecognitionResult(event);
        } else if ("task-finished".equals(eventName)) {
            resetTaskState();
            resultListener.onTaskFinished();
        } else if ("task-failed".equals(eventName)) {
            String message = header.optString("error_message", "Recognition failed");
            resetTaskState();
            resultListener.onError(message);
        }
    }

    private void handleRecognitionResult(JSONObject event) {
        JSONObject payload = event.optJSONObject("payload");
        JSONObject output = payload == null ? null : payload.optJSONObject("output");
        JSONObject sentence = output == null ? null : output.optJSONObject("sentence");
        if (sentence == null || sentence.optBoolean("heartbeat", false)) {
            return;
        }
        String text = sentence.optString("text", "");
        if (!text.isEmpty()) {
            resultListener.onResult(text, sentence.optBoolean("sentence_end", false));
        }
    }

    private void resetTaskState() {
        engine.enableSendMediaStream(AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, false);
        taskStarted = false;
        taskId = null;
    }

    private JSONObject createHeader(String action) {
        return new JSONObject()
                .put("action", action)
                .put("task_id", taskId)
                .put("streaming", "duplex");
    }

    private void send(JSONObject event) {
        AoqClientEngine.AoqDataMsg msg = new AoqClientEngine.AoqDataMsg();
        msg.data = event.toString().getBytes(StandardCharsets.UTF_8);
        engine.sendDataMsg(msg);
    }

    private static void addTrack(AoqClientEngine.AoqConnectConfig config, boolean publish,
                                 AoqClientEngine.AoqTrackType type) {
        AoqClientEngine.AoqTrackParam track = new AoqClientEngine.AoqTrackParam();
        track.trackType = type;
        if (publish) {
            config.publishTracks.add(track);
        } else {
            config.subscribeTracks.add(track);
        }
    }

    /** Maps the AppServer token response to an SDK connection configuration. */
    public static AoqClientEngine.AoqConnectConfig parseConnectConfig(String responseText) {
        JSONObject response = new JSONObject(responseText);
        AoqClientEngine.AoqConnectConfig config = new AoqClientEngine.AoqConnectConfig();
        config.token = response.optString("aoqTokenForClient", "");
        config.sid = response.optString("sid", "");
        config.certFingerprint = response.optString("clientRelayCertFingerprint", "");

        JSONArray endpoints = response.optJSONArray("clientRelayEndpoints");
        if (endpoints != null) {
            for (int i = 0; i < endpoints.length(); i++) {
                JSONObject item = endpoints.optJSONObject(i);
                if (item == null) {
                    continue;
                }
                AoqClientEngine.AoqRelayEndpoint endpoint =
                        new AoqClientEngine.AoqRelayEndpoint();
                endpoint.routeIndex = item.has("route_index")
                        ? item.optInt("route_index", i) : i;
                endpoint.endpoint = item.optString("endpoint", "");
                endpoint.port = item.optInt("port", 0);
                config.relayEndpoints.add(endpoint);
            }
        }

        JSONObject extraInfo = response.optJSONObject("extraInfo");
        config.workspaceIdHash = extraInfo == null
                ? "" : extraInfo.optString("workspaceIdHash", "");
        return config;
    }
}

Exemplo de uso

Passe a resposta do token do servidor de aplicativos para parseConnectConfig e crie o cliente. A primeira tarefa de reconhecimento inicia automaticamente após o estabelecimento da conexão. O botão de parar encerra apenas a tarefa atual. Libere a conexão e os recursos locais quando a página for destruída.
private AsrClient client;

void startRecognition(Context context, String tokenResponseText) {
    AoqClientEngine.AoqConnectConfig config =
            AsrClient.parseConnectConfig(tokenResponseText);

    client = new AsrClient(context, config, new AsrClient.ResultListener() {
        @Override
        public void onResult(String text, boolean sentenceEnd) {
            // Update the UI with the partial or final sentence.
        }

        @Override
        public void onTaskFinished() {
            // Enable the start button or call beginRecognition() for another task.
        }

        @Override
        public void onError(String message) {
            // Display or log the error.
        }
    });
}

void onStopButtonClick() {
    // Finish the task and keep the AOQ connection open until task-finished arrives.
    client.finishRecognition();
}

void onPageDestroyed() {
    // Release local resources only when the page is closed.
    client.close();
}

Executar e verificar

  1. Inicie o servidor de aplicativos. Certifique-se de que a solicitação de token retorne HTTP 200 e inclua sid, aoqTokenForClient, clientRelayEndpoints, clientRelayCertFingerprint e extraInfo.workspaceIdHash.
  2. Instale e execute o aplicativo em um dispositivo Android, conceda acesso ao microfone e fale.
  3. Observe os callbacks. Uma sequência normal de eventos é:
task-started
result-generated (sentence_end=false)
result-generated (sentence_end=true)
task-finished
Transcrições parciais são retornadas enquanto você fala. Após chamar finishRecognition, a transcrição final da frase atual e task-finished são retornados. Não desconecte imediatamente após o envio de finish-task.

Cenários comuns

Várias tarefas de reconhecimento em uma única conexão

Após receber task-finished, chame beginRecognition para iniciar outra tarefa na mesma conexão AOQ. Cada tarefa requer um novo task_id. Não é necessário solicitar outro token ou reconectar enquanto a conexão permanecer ativa. Se a conexão for fechada, obtenha novas credenciais de conexão.

Reconhecimento em segundo plano no Android

No Android 10 ou posterior, use um serviço em primeiro plano com foregroundServiceType=microphone para continuar a captura do microfone enquanto o aplicativo estiver em segundo plano. Inicie o serviço enquanto o aplicativo ainda estiver visível para o usuário.

Solução de problemas

Problema

Solução

Falha na conexão

Verifique se o token é válido, se o endpoint corresponde à região da aplicação e se o servidor de aplicativos passa o endereço IP público real do dispositivo cliente. Não reutilize um token após o fechamento da conexão.

A tarefa inicia, mas nenhuma transcrição é retornada

Ative o envio pela track de Audio apenas após receber task-started. Verifique o formato de entrada, taxa de amostragem e outros parâmetros em relação a Client events para o modelo.

A transcrição final não é retornada

Desative o envio pela track de Audio antes de enviar finish-task. Aguarde o evento final result-generated e task-finished em vez de desconectar imediatamente.

Falha ao carregar o SDK Android

Certifique-se de que o AAR esteja incluído como dependência e que o aplicativo empacote uma ABI suportada pelo SDK: armeabi-v7a ou arm64-v8a.

Uma tarefa subsequente na mesma conexão é rejeitada

Confirme que task-finished foi recebido para a tarefa anterior e gere um novo task_id para o próximo evento run-task.

Tópicos relacionados

Para todos os parâmetros, campos de eventos e APIs para outras plataformas, consulte:
Plano de Tokens
Playground de Modelos
  • Music generation
Inferência do Modelo
Avaliação
Compressão de Modelos
Estatísticas e Monitoramento
Suporte
Transcrever fala em tempo real com fun-asr-realtime via AOQ - Alibaba Cloud Model Studio