Guía para Desarrolladores de Java: Envoltura de API Nativa para SenseVoice-Small ONNX

Como desarrollador de Java, es común encontrarse con la necesidad de integrar funcionalidades de reconocimiento de voz en aplicaciones. SenseVoice-Small es un modelo eficiente de reconocimiento de voz multilingüe que soporta idiomas como chino, inglés, japonés y coreano, superando a modelos como Whisper. Sin embargo, el soporte oficial se centra principalmente en Python. Este artículo te guiará en cómo encapsular el modelo ONNX de SenseVoice-Small como una API nativa en Java, cubriendo aspectos como la integarción JNI, la gestión de memoria y la optimización multihilo, junto con un ejemplo de proyecto Maven.

  1. Preparación del Entorno y Configuración del Proyecto

2.1 Requisitos del Sistema y Dependencias

Asegúrate de tener los siguientes componentes instalados:

  • JDK 11 o superior
  • Maven 3.6+
  • ONNX Runtime 1.15+
  • CMake 3.10+ (para compilar las bibliotecas nativas)

2.2 Creacción del Proyecto Maven

Inicia creando un proyecto Maven estándar y añade las dependencias necesarias:

<dependencies>
   <dependency>
       <groupId>com.microsoft.onnxruntime</groupId>
       <artifactId>onnxruntime</artifactId>
       <version>1.15.1</version>
   </dependency>
   <dependency>
       <groupId>net.java.dev.jna</groupId>
       <artifactId>jna</artifactId>
       <version>5.13.0</version>
   </dependency>
   <dependency>
       <groupId>org.apache.commons</groupId>
       <artifactId>commons-math3</artifactId>
       <version>3.6.1</version>
   </dependency>
</dependencies>

2.3 Preparación de los Archivos del Modelo

Descarga los archivos del modelo ONNX de SenseVoice-Small desde ModelScope o HuggingFace:

  • sanse-voice-encoder.onnx
  • sense-voice-decoder.onnx
  • tokens.txt (vocabulario)
  • am.mvn (archivo de normalización de características)

Coloca estos archivos en el directorio src/main/resources/models de tu proyecto.

  1. Diseño de la Arquitectura Central

3.1 Diseño de la Capa de Encapsulamiento JNI

Para una interacción eficiente entre Java y ONNX Runtime, se ha diseñado una capa intermediaria JNI:

public class SenseVoiceNative {
   static {
       System.loadLibrary("sensevoice_jni");
   }

   public native long iniciarSesion(String rutaModelo, String rutaTokens, int hilos);
   public native String reconocer(long handleSesion, float[] datosAudio, int frecuenciaMuestreo);
   public native void liberarSesion(long handleSesion);
}

3.2 Estrategia de Gestión de Memoria

El manejo de grandes cantidades de datos de audio requiere una cuidadosa gestión de la memoria:

public class SesionSenseVoice implements AutoCloseable {
   private long handleNativo;
   private boolean cerrada = false;

   public SesionSenseVoice(String rutaModelo, String rutaTokens) {
       this.handleNativo = SenseVoiceNative.iniciarSesion(rutaModelo, rutaTokens, 4);
   }

   public String reconocer(float[] datosAudio, int frecuenciaMuestreo) {
       if (cerrada) {
           throw new IllegalStateException("Sesión ya cerrada");
       }
       return SenseVoiceNative.reconocer(handleNativo, datosAudio, frecuenciaMuestreo);
   }

   @Override
   public void close() {
       if (!cerrada) {
           SenseVoiceNative.liberarSesion(handleNativo);
           cerrada = true;
       }
   }

   @Override
   protected void finalize() throws Throwable {
       try {
           close();
       } finally {
           super.finalize();
       }
   }
}

3.3 Módulo de Preprocesamiento de Audio

Antes del reconocimiento, el audio debe ser preprocesado:

public class ProcesadorAudio {
   public static float[] extraerCaracteristicasFbank(byte[] datosAudio, int frecuenciaMuestreo) {
       float[] datosFloat = convertirAFloat(datosAudio);

       datosFloat = enfasisPre(datosFloat);
       float[][] frames = enmarcarSeñal(datosFloat, frecuenciaMuestreo);
       return calcularFbank(frames, frecuenciaMuestreo);
   }

   private static float[] convertirAFloat(byte[] datosAudio) {
       float[] datosFloat = new float[datosAudio.length / 2];
       for (int i = 0; i < datosFloat.length; i++) {
           short muestra = (short) ((datosAudio[2 * i] & 0xFF) | (datosAudio[2 * i + 1] << 8));
           datosFloat[i] = muestra / 32768.0f;
       }
       return datosFloat;
   }
}
  1. Implementación Completa del Encapsulamiento de la API

4.1 Clase de Reconocimiento Central

Crea una clase fácil de usar para el reconocimiento de voz:

public class ReconocedorSenseVoice {
   private final SesionSenseVoice sesion;
   private final ProcesadorAudio procesadorAudio;

   public ReconocedorSenseVoice(String directorioModelo) {
       String rutaModelo = Paths.get(directorioModelo, "sense-voice-encoder.onnx").toString();
       String rutaTokens = Paths.get(directorioModelo, "tokens.txt").toString();
       this.sesion = new SesionSenseVoice(rutaModelo, rutaTokens);
       this.procesadorAudio = new ProcesadorAudio();
   }

   public ResultadoReconocimiento reconocer(File archivoAudio) throws IOException {
       return reconocer(Files.readAllBytes(archivoAudio.toPath()), 16000);
   }

   public ResultadoReconocimiento reconocer(byte[] datosAudio, int frecuenciaMuestreo) {
       float[] caracteristicas = procesadorAudio.extraerCaracteristicasFbank(datosAudio, frecuenciaMuestreo);
       String texto = sesion.reconocer(caracteristicas, frecuenciaMuestreo);
       return new ResultadoReconocimiento(texto, System.currentTimeMillis());
   }

   public List<resultadoreconocimiento> reconocerLote(List<file> archivosAudio) {
       return archivosAudio.parallelStream()
               .map(file -> {
                   try {
                       return reconocer(file);
                   } catch (IOException e) {
                       return new ResultadoReconocimiento("", -1, e.getMessage());
                   }
               })
               .collect(Collectors.toList());
   }
}</file></resultadoreconocimiento>

4.2 Clase de Envoltura de Resultados

public class ResultadoReconocimiento {
   private final String texto;
   private final long tiempoProceso;
   private final String error;

   public ResultadoReconocimiento(String texto, long tiempoProceso) {
       this(texto, tiempoProceso, null);
   }

   public ResultadoReconocimiento(String texto, long tiempoProceso, String error) {
       this.texto = texto;
       this.tiempoProceso = tiempoProceso;
       this.error = error;
   }

   public boolean isExitoso() { return error == null; }
   public String getTexto() { return texto; }
   public long getTiempoProceso() { return tiempoProceso; }
   public String getError() { return error; }
}
  1. Optimización Multihilo y Rendimiento

5.1 Gestión de Pools de Hilos

Para escenarios de alta concurrencia, es necesario un pool de hilos adecuado:

public class ServicioSenseVoice {
   private final ExecutorService poolReconocimiento;
   private final ReconocedorSenseVoice reconocedor;
   private final int maxSesionesConcurrentes;

   public ServicioSenseVoice(String directorioModelo, int maxHilos) {
       this.reconocedor = new ReconocedorSenseVoice(directorioModelo);
       this.maxSesionesConcurrentes = maxHilos;
       this.poolReconocimiento = Executors.newFixedThreadPool(maxHilos);
   }

   public CompletableFuture<resultadoreconocimiento> reconocerAsincrono(File archivoAudio) {
       return CompletableFuture.supplyAsync(() -> {
           try {
               return reconocedor.reconocer(archivoAudio);
           } catch (IOException e) {
               return new ResultadoReconocimiento("", -1, e.getMessage());
           }
       }, poolReconocimiento);
   }
}</resultadoreconocimiento>

5.2 Optimización del Pool de Sesiones

Para evitar el costo de crear y destruir sesiones con frecuencia, implementa un pool de sesiones:

public class PoolSesiones {
   private final BlockingQueue<sesionsensevoice> pool;
   private final String rutaModelo;
   private final String rutaTokens;
   private final int maxSize;

   public PoolSesiones(String rutaModelo, String rutaTokens, int sizePool) {
       this.rutaModelo = rutaModelo;
       this.rutaTokens = rutaTokens;
       this.maxSize = sizePool;
       this.pool = new LinkedBlockingQueue<>(sizePool);

       for (int i = 0; i < sizePool; i++) {
           pool.offer(crearSesion());
       }
   }

   public SesionSenseVoice tomarSesion() throws InterruptedException {
       SesionSenseVoice sesion = pool.poll();
       if (sesion != null) {
           return sesion;
       }
       return crearSesion();
   }

   public void devolverSesion(SesionSenseVoice sesion) {
       if (!pool.offer(sesion)) {
           sesion.close(); // El pool está lleno, cierra la sesión
       }
   }

   private SesionSenseVoice crearSesion() {
       return new SesionSenseVoice(rutaModelo, rutaTokens);
   }
}</sesionsensevoice>
  1. Ejemplos de Uso Completos

6.1 Uso Básico

public class EjemploBasico {
   public static void main(String[] args) {
       String directorioModelo = "src/main/resources/models";

       try (ReconocedorSenseVoice reconocedor = new ReconocedorSenseVoice(directorioModelo)) {
           File archivoAudio = new File("test_audio.wav");

           long startTime = System.currentTimeMillis();
           ResultadoReconocimiento resultado = reconocedor.reconocer(archivoAudio);
           long endTime = System.currentTimeMillis();

           if (resultado.isExitoso()) {
               System.out.println("Resultado: " + resultado.getTexto());
               System.out.println("Tiempo de proceso: " + (endTime - startTime) + "ms");
           } else {
               System.out.println("Error: " + resultado.getError());
           }
       }
   }
}

6.2 Ejemplo de Alta Concorrencia

public class EjemploConcorrencia {
   public static void main(String[] args) throws Exception {
       ServicioSenseVoice servicio = new ServicioSenseVoice("models", 4);
       List<file> archivosAudio = Arrays.asList(
           new File("audio1.wav"),
           new File("audio2.wav"),
           new File("audio3.wav")
       );

       List<completablefuture>> futuros = archivosAudio.stream()
               .map(servicio::reconocerAsincrono)
               .collect(Collectors.toList());

       CompletableFuture.allOf(futuros.toArray(new CompletableFuture[0])).join();

       futuros.forEach(futuro -> {
           try {
               ResultadoReconocimiento resultado = futuro.get();
               System.out.println("Resultado: " + resultado.getTexto());
           } catch (Exception e) {
               System.err.println("Error: " + e.getMessage());
           }
       });

       servicio.shutdown();
   }
}</completablefuture></file>
  1. Problemas Comunes y Soluciones

7.1 Manejo de Fugas de Memoria

En la capa JNI, es crucial manejar la memoria correctamente:

JNIEXPORT jstring JNICALL Java_SenseVoiceNative_reconocer
 (JNIEnv *env, jobject obj, jlong handleSesion, jfloatArray datosAudio, jint frecuenciaMuestreo) {

   jfloat* arrayAudio = (*env)->GetFloatArrayElements(env, datosAudio, NULL);
   jsize length = (*env)->GetArrayLength(env, datosAudio);

   try {
       const char* resultado = procesarAudio(handleSesion, arrayAudio, length, frecuenciaMuestreo);
       jstring jresultado = (*env)->NewStringUTF(env, resultado);

       (*env)->ReleaseFloatArrayElements(env, datosAudio, arrayAudio, JNI_ABORT);
       return jresultado;
   } catch (...) {
       (*env)->ReleaseFloatArrayElements(env, datosAudio, arrayAudio, JNI_ABORT);
       lanzarExcepcion(env, "Reconocimiento fallido");
       return NULL;
   }
}

7.2 Recomendaciones de Optimización de Rendimiento

  • Procesamiento por Lotes: Trata de procesar archivos de audio en lotes para reducir el costo de creación de sesiones.
  • Pre-calentamiento: Carga algunas sesiones al inicio de la aplicación.
  • Monitoreo de Memoria: Verifica regularmente el uso de memoria nativa.
  • Control de Tiempos de Espera: Establece tiempos de espera para evitar bloqueos prolongados.

7.3 Estrategias de Manejo de Excepciones

public class ExcepcionSenseVoice extends RuntimeException {
   public ExcepcionSenseVoice(String mensaje) { super(mensaje); }
   public ExcepcionSenseVoice(String mensaje, Throwable causa) { super(mensaje, causa); }
}

public class ReconocedorSenseVoice {
   public ResultadoReconocimiento reconocerSeguro(File archivoAudio) {
       try {
           return reconocer(archivoAudio);
       } catch (IOException e) {
           throw new ExcepcionSenseVoice("Error al leer el archivo de audio", e);
       } catch (Exception e) {
           throw new ExcepcionSenseVoice("Error en el reconocimiento de voz", e);
       }
   }
}

Etiquetas: java ONNX JNI reconocimiento de voz SenseVoice

Publicado el 10-3 16:06