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.
- 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.
- 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;
}
}
- 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; }
}
- 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>
- 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>
- 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);
}
}
}