Skip to main content

Transcribir voz en tiempo real con fun-asr-realtime a través de AOQ

Utilice AOQ para conectarse a fun-asr-realtime, transmitir audio del micrófono y recibir resultados de transcripción en tiempo real. El código del cliente utiliza Java para Android, y otras plataformas compatibles con AOQ proporcionan las mismas interfaces.

Descripción general de la solución

fun-asr-realtime transcribe una secuencia de audio en texto puntuado en tiempo real. El SDK de AOQ transporta medios y eventos a través de pistas separadas: el cliente envía audio por la pista de Audio y utiliza la pista de Datos para enviar eventos de control y recibir eventos de reconocimiento. El modelo utiliza el protocolo de eventos Inference, no el protocolo de eventos Realtime. Esta solución es adecuada para subtítulos en vivo, transcripción de reuniones, entrada de voz y asistentes inteligentes. La pista de Audio elimina la necesidad de codificar audio en mensajes de eventos, mientras que la pista de Datos conserva la semántica de tareas como run-task, result-generated y finish-task.
  1. El cliente solicita credenciales de conexión AOQ temporales al servidor de aplicaciones.
  2. El servidor de aplicaciones utiliza una clave de API para solicitar un token a Model Studio y devuelve los campos de conexión al cliente.
  3. El cliente establece una conexión AOQ y envía run-task. Después de recibir task-started, el cliente comienza a enviar audio del micrófono.
  4. El servidor devuelve continuamente eventos result-generated. Después de que el cliente envía finish-task, espera el resultado final y task-finished.

Requisitos previos

  1. Habilite Alibaba Cloud Model Studio y cree una clave de API como se describe en Obtención y configuración de una clave de API. Almacene la clave de API únicamente en el servidor de aplicaciones. No la incluya en el código del cliente ni la confirme en un repositorio de código.
  2. Seleccione un endpoint de AOQ para la región donde está implementada su aplicación. Para la selección de endpoints, consulte Selección de región, alcance de implementación y endpoint.
  3. Descargue la versión más reciente del AOQ Client SDK como se describe en Descarga del SDK. Este tutorial transporta audio Opus y requiere el complemento Opus.
  4. Configure un AppServer según los Pasos de autenticación de AOQ para Fun-ASR-Realtime. Antes de cada conexión nueva, el cliente debe obtener credenciales de conexión nuevas de AppServer.

Importar el SDK

Seleccione las instrucciones de integración del SDK correspondientes a su plataforma de desarrollo. La implementación del cliente utiliza Android Java; otras plataformas siguen el mismo diseño de API y flujo de eventos.
  • Android
  • iOS
  • HarmonyOS
  • Linux (Python)
  1. Coloque libPluginOpus.so del complemento Opus en app/src/main/jniLibs/armeabi-v7a/ y app/src/main/jniLibs/arm64-v8a/ para las ABI correspondientes. Coloque AoqClientSdk-release.aar en app/libs y configure la dependencia y las ABI en app/build.gradle:
android {
    defaultConfig {
        minSdk 21
        ndk { abiFilters 'armeabi-v7a', 'arm64-v8a' }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.aar'])
}
  1. Declare los permisos de red y grabación en 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 el permiso de ejecución RECORD_AUDIO antes de iniciar la grabación. El reconocimiento de voz no requiere el permiso CAMERA.

Probar la demostración

Utilice la demostración de Android de Alibaba Cloud Model Studio para verificar rápidamente la conectividad AOQ. Descargue el APK y configure la clave de API y workspaceId para probar modelos seleccionados. Escanee el siguiente código QR para descargar la demostración:
Código QR para descargar la demostración

Flujo de implementación

  1. El servidor de aplicaciones utiliza la URL de token de inferencia para obtener los parámetros de conexión AOQ para fun-asr-realtime.
  2. El cliente asigna la respuesta del token a AoqConnectConfig, publica las pistas de Audio y Datos, y se suscribe a la pista de Datos.
  3. El cliente configura los parámetros de codificación de audio según los requisitos del negocio y del modelo, inicia la captura del micrófono con el envío de audio deshabilitado y establece la conexión AOQ.
  4. Una vez establecida la conexión, el cliente envía run-task y habilita el envío de la pista de Audio después de recibir task-started.
  5. El cliente maneja result-generated en onDataMsg. Para detener el reconocimiento, primero deshabilita el envío de la pista de Audio y luego envía finish-task.
  6. Después de recibir task-finished, el cliente puede iniciar otra tarea en la misma conexión con un nuevo task_id, o desconectarse y destruir el motor.
Diagrama de secuencia para el reconocimiento de voz en tiempo real con fun-asr-realtime a través de AOQ

Obtener un token en el servidor de aplicaciones

Establezca DASHSCOPE_API_KEY en el servidor de aplicaciones y envíe una solicitud al endpoint de la región seleccionada. clientIp es la dirección IP pública real del dispositivo cliente. Este campo es opcional, pero le recomendamos proporcionarlo para que el servicio pueda seleccionar un endpoint de retransmisión adecuado.
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}\"}"
Si el servidor de aplicaciones no puede obtener la dirección IP pública real del dispositivo cliente, omita clientIp del cuerpo de la solicitud en lugar de pasar una cadena vacía.
AppServer devuelve los siguientes campos de respuesta al cliente. Almacene la API Key únicamente en AppServer.

Campo de respuesta

Campo del SDK

aoqTokenForClient

AoqConnectConfig.token

sid

AoqConnectConfig.sid

clientRelayCertFingerprint

AoqConnectConfig.certFingerprint

clientRelayEndpoints

AoqConnectConfig.relayEndpoints

extraInfo.workspaceIdHash

AoqConnectConfig.workspaceIdHash

Implementar el cliente de Android

Los siguientes pasos dividen el código del cliente de Android Java según la secuencia real de conexión y tareas. Cada fragmento se toma del ejemplo completo que se presenta más adelante en este tema.

1. Crear el motor y registrar devoluciones de llamada

Cree un motor de cliente AOQ y registre callbacks para los cambios de estado de conexión y los eventos de Data-track. Implemente la lógica de callbacks para su aplicación e inicie el reconocimiento únicamente después de que se haya establecido la conexión.
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 la codificación de audio

Configure la codificación de audio enviada al modelo. Establezca el formato, la frecuencia de muestreo y el número de canales según los requisitos de su aplicación y del modelo. El siguiente código utiliza Opus mono de 16 kHz como ejemplo. Para conocer los valores admitidos, consulte los parámetros de run-task en Eventos del cliente.
AoqClientEngine.AoqAudioCodecConfig encoder =
        new AoqClientEngine.AoqAudioCodecConfig();
encoder.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
encoder.codecType = AoqClientEngine.AoqEncoderType.AoqEncoderTypeAudioOpus;
encoder.sampleRate = 16000;
encoder.channel = 1;
encoder.bitrate = 24000;
engine.setAudioEncoderConfig(encoder);

3. Configurar la conexión y las pistas de transporte

Configure la conexión AOQ con las credenciales devueltas por el servidor de aplicaciones y seleccione las pistas para publicar y suscribirse según su aplicación. Para el reconocimiento de voz en tiempo real, este ejemplo publica pistas de Audio y Datos, y se suscribe a la pista de Datos.
addTrack(connectConfig, true,
        AoqClientEngine.AoqTrackType.AoqTrackTypeAudio);
addTrack(connectConfig, true,
        AoqClientEngine.AoqTrackType.AoqTrackTypeData);
addTrack(connectConfig, false,
        AoqClientEngine.AoqTrackType.AoqTrackTypeData);

4. Iniciar la captura de audio y establecer la conexión

Configure la captura de audio y establezca la conexión AOQ. Elija captura integrada o externa, si desea habilitar el modo VoIP y el número de canales según su aplicación. Mantenga deshabilitado el envío de Audio-track hasta que se reciba 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 una tarea de reconocimiento

Una vez establecida la conexión, genere un ID de tarea y envíe run-task para iniciar el reconocimiento. Configure model, format, sample_rate y otros parámetros de tarea para el modelo y la entrada de audio que utilice. Para más detalles, consulte Eventos del cliente.
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. Manejar eventos del servidor

Gestione los estados de las tareas, los resultados de reconocimiento y los errores, y pase los resultados a su aplicación. Implemente callbacks según sus requisitos de interfaz de usuario y gestión de estados. Envíe audio solo después de recibir task-started y excluya los eventos de latido de las transcripciones mostradas. Para consultar el esquema completo de respuestas, vea Eventos del servidor.
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. Finalizar la tarea de reconocimiento

Cuando el usuario finalice la grabación actual, deje de enviar audio y envíe finish-task. Mantenga la conexión abierta hasta que lleguen la transcripción final y task-finished. Luego puede iniciar otra tarea o liberar la conexión según el flujo de su aplicación. Para consultar el formato de eventos, vea Eventos del cliente.
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 y destruir el motor

Libere la captura de audio, la conexión AOQ y los recursos del motor cuando se destruya la página o ya no se necesite el reconocimiento. Elija el punto de liberación según el ciclo de vida de su aplicación y no libere los recursos inmediatamente después de enviar finish-task.
engine.enableSendMediaStream(
        AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, false);
engine.stopAudioCapture();
engine.disconnect();
AoqClientEngine.destroy();

Ejemplo completo

Esta clase de Android Java asigna la respuesta JSON del servidor de aplicaciones a AoqConnectConfig y combina la lógica anterior de conexión, captura, tarea y liberación 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.AoqEncoderTypeAudioOpus;
        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;
    }
}

Ejemplo de uso

Pase la respuesta del token del servidor de aplicaciones a parseConnectConfig y cree el cliente. La primera tarea de reconocimiento se inicia automáticamente después de establecer la conexión. El botón de detener finaliza únicamente la tarea actual. Libere la conexión y los recursos locales cuando se destruya la página.
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();
}

Ejecutar y verificar

  1. Inicie el servidor de aplicaciones. Asegúrese de que la solicitud de token devuelva HTTP 200 e incluya sid, aoqTokenForClient, clientRelayEndpoints, clientRelayCertFingerprint y extraInfo.workspaceIdHash.
  2. Instale y ejecute la aplicación en un dispositivo Android, conceda acceso al micrófono y hable.
  3. Observe las devoluciones de llamada. Una secuencia normal de eventos es:
task-started
result-generated (sentence_end=false)
result-generated (sentence_end=true)
task-finished
Se devuelven transcripciones parciales mientras usted habla. Después de llamar a finishRecognition, se devuelven la transcripción final de la oración actual y task-finished. No se desconecte inmediatamente después de enviar finish-task.

Escenarios comunes

Múltiples tareas de reconocimiento en una sola conexión

Después de recibir task-finished, llame a beginRecognition para iniciar otra tarea sobre la misma conexión AOQ. Cada tarea requiere un nuevo task_id. No necesita solicitar otro token ni volver a conectarse mientras la conexión permanezca activa. Si se cierra la conexión, obtenga nuevas credenciales de conexión.

Reconocimiento en segundo plano en Android

En Android 10 o versiones posteriores, utilice un servicio en primer plano con foregroundServiceType=microphone para continuar la captura del micrófono mientras la aplicación se encuentra en segundo plano. Inicie el servicio mientras la aplicación aún sea visible para el usuario.

Solución de problemas

Problema

Solución

La conexión falla

Asegúrese de que el token sea válido, de que el endpoint coincida con la región de la aplicación y de que el servidor de aplicaciones transmita la dirección IP pública real del dispositivo cliente. No reutilice un token después de cerrar la conexión.

La tarea inicia pero no se devuelve ninguna transcripción

Habilite el envío de Audio-track solo después de recibir task-started. Verifique el formato de entrada, la frecuencia de muestreo y otros parámetros en Eventos del cliente correspondientes al modelo.

No se devuelve la transcripción final

Deshabilite el envío de la pista de Audio antes de enviar finish-task. Espere el último evento result-generated y task-finished en lugar de desconectarse inmediatamente.

El SDK de Android no se carga

Asegúrese de que el AAR esté incluido como dependencia y de que la aplicación empaquete una ABI compatible con el SDK: armeabi-v7a o arm64-v8a.

Se rechaza una tarea posterior sobre la misma conexión

Asegúrese de haber recibido task-finished para la tarea anterior y genere un nuevo task_id para el siguiente evento run-task.

Temas relacionados

Para todos los parámetros, campos de eventos y API de otras plataformas, consulte: