Diarización local de hablantes: cómo creamos un tomador de notas de reuniones 100 % privado
OpenWhispr es el asistente de reuniones de código abierto donde tu audio nunca sale de tu dispositivo. Cada huella de voz, cada etiqueta de hablante, cada embedding: procesado y almacenado en tu propia máquina.
OpenWhispr
Ingeniería
Tabla de contenidos
La diarización local de hablantes es el proceso de etiquetar quién habló y cuándo en la grabación de una reunión, ejecutado por completo en el dispositivo del usuario, sin que ningún audio, embedding o transcripción se transmita a ningún servidor externo. Las transcripciones de reuniones tienen un problema de «quién dijo qué». Zoom te entrega un muro de texto. La mayoría de los tomadores de notas con IA —Otter, Fireflies, Read, Granola— lo resuelven subiendo el audio de tu reunión a sus servidores, ejecutando allí un modelo de hablantes y devolviendo los segmentos etiquetados. Tu voz, y las voces de tus colegas, acaban como vectores en la base de datos de otra persona. Nosotros no queríamos eso, y nuestros usuarios tampoco. OpenWhispr es el tomador de notas de reuniones de código abierto construido sobre la premisa contraria: el audio nunca sale de tu dispositivo, las huellas de voz residen en un archivo SQLite local en tu disco y nada de una reunión se sube a menos que tú lo elijas explícitamente. Este artículo es el desglose técnico del lado de la diarización de esa promesa: qué es, qué modelos elegimos, cómo encajan entre sí y exactamente dónde reside cada byte de datos en cada paso.
Última actualización: 15 de abril de 2026. Los detalles de implementación hacen referencia a la función de diarización de hablantes fusionada en la app de escritorio de OpenWhispr el 14 de abril de 2026. Cada afirmación técnica que figura a continuación procede de referencias primarias o es directamente inspeccionable en el repositorio de código abierto.
Verificación rápida (fuentes duales)
- La diarización de hablantes responde a «quién habló y cuándo». Se evalúa frente a benchmarks comunitarios como NIST Rich Transcription y la canalización de referencia de pyannote.audio. Evaluación NIST RT · Artículo de pyannote.audio (Bredin et al., 2020)
- Usamos pyannote-segmentation-3.0 y CAM++ del proyecto 3D-Speaker. Ambos son de código abierto, ambos son portables a ONNX y ambos se descargan directamente desde las publicaciones de modelos de sherpa-onnx. Ficha del modelo pyannote-segmentation-3.0 · Artículo de CAM++ / 3D-Speaker (Wang et al., 2023)
- Toda la inferencia se ejecuta a través de sherpa-onnx, un binario ONNX nativo: sin entorno de Python, sin PyTorch, sin GPU requerida. Fijamos la versión del binario y sus dependencias en tiempo de compilación. sherpa-onnx en GitHub · Publicaciones de sherpa-onnx
- Las huellas de voz son vectores float32 de 512 dimensiones almacenados como BLOB de SQLite locales, unos 2 KB por hablante. Nunca salen del dispositivo. Documentación de tipos de datos de SQLite · ONNX Runtime
- Ningún audio, ningún embedding y ninguna transcripción se transmiten fuera del dispositivo durante la diarización. La única actividad de red en toda la función es una descarga única del modelo en el primer inicio, que puedes inspeccionar tú mismo en el repositorio de código abierto. Política de privacidad de OpenWhispr · OpenWhispr en GitHub
Qué es realmente la diarización de hablantes
La diarización de hablantes es el proceso de etiquetar quién habló y cuándo en una grabación, no qué dijeron. La transcripción responde al qué; la diarización responde al quién. Ambas son complementarias: un buen tomador de notas de reuniones necesita las dos, y conseguir que funcionen juntas es más difícil que cualquiera de ellas por separado.
La diarización es genuinamente difícil por un puñado de razones tercas. Dos voces pueden solaparse en una misma llamada. El ruido de fondo se cuela. Las intervenciones cortas («sí», «vale») apenas aportan información del hablante. Aparecen nuevos hablantes a mitad de la reunión. Y, a diferencia de la transcripción, la diarización tiene que tomar decisiones globales: no puede etiquetar el segmento siete sin compararlo con el segmento dos. Ese contexto global es la razón por la que la mayoría de las soluciones comerciales hacen la diarización en el servidor sobre la grabación completa y luego devuelven la transcripción etiquetada.
La transcripción por sí sola no basta. Una reunión de diez personas con cuarenta y cinco minutos de conversaciones cruzadas, representada como un muro de texto indiferenciado, es ilegible. Las notas de reuniones —el producto real que la gente quiere— requieren atribución de hablantes, porque las tareas, las decisiones y los compromisos están todos vinculados a personas. «Alice entregará la migración antes del viernes» es una nota. «Entregará la migración antes del viernes» es ruido.
La diarización no es lo mismo que el reconocimiento de hablantes
La diarización dice «estos tres fragmentos son del Hablante A; estos dos son del Hablante B». No sabe quién es el Hablante A. El reconocimiento de hablantes pone un nombre al Hablante A emparejando una huella de voz con un perfil almacenado. OpenWhispr hace ambas cosas: la diarización se ejecuta en cada reunión, y el reconocimiento entra en juego una vez que has etiquetado a alguien con su nombre. Las dos permanecen en local.
La canalización local de cuatro etapas
La canalización de diarización de hablantes de OpenWhispr tiene cuatro etapas: detección de actividad de voz, segmentación, embedding de hablante y clustering, seguidas de un paso de fusión que reconcilia los segmentos de hablante con la transcripción. Cada etapa es un modelo ONNX o un binario nativo independiente. Sin entorno de Python, sin PyTorch, sin CUDA. La huella total en disco de los modelos es de unos 45 MB.
El canal de diarización local
Por qué sigue siendo local:
Cada etapa se ejecuta en su dispositivo a través de un único binario sherpa-onnx: sin Python, sin nube.
~45MB de ONNX modelos descargados una vez y luego almacenados en caché en ~/.cache/openwhispr/diarization-models/.
Las incrustaciones son vectores 512-dim float32 almacenados como SQLite BLOBs en su disco local.
La captura de audio es de doble flujo desde el primer día. El micrófono es, por definición, «tú», así que nunca gastamos cómputo intentando diarizar tu propia voz: cada segmento del micrófono se etiqueta como tú por origen, no por coincidencia de voz. El audio del sistema (todos los demás en la llamada) es lo único que realmente necesita separación por hablante, lo que de inmediato reduce el trabajo a la mitad.
Una vez capturado el audio, las cuatro etapas se ejecutan en secuencia. La VAD descarta el silencio para que la costosa etapa de embedding nunca desperdicie ciclos en fotogramas vacíos. La segmentación corta el habla continua en fragmentos de un solo hablante. El modelo de embedding convierte cada fragmento en una huella de voz de 512 dimensiones. El clustering aglomerativo agrupa las huellas que se parecen, produciendo un conjunto final de IDs de hablante. Una última pasada fusiona esos IDs de vuelta en la transcripción por solapamiento de marcas de tiempo, de modo que cada palabra acaba etiquetada con un hablante.
Todo se ejecuta dentro de un único proceso hijo generado desde la app: sin servidor, sin puerto de escucha, sin IPC hacia nada fuera de Electron. Puedes confirmarlo leyendo src/helpers/diarization.js en el repositorio de código abierto.
Etapa 1: Detección de actividad de voz (Silero)
La detección de actividad de voz (VAD) es el filtro barato que separa el habla del silencio antes de que se ejecuten las etapas costosas. Ejecutar un modelo de embedding de hablante sobre silencio es CPU desperdiciada y produce vectores basura. La VAD es lo que mantiene rápido el resto de la canalización.
Usamos Silero VAD: un modelo ONNX de 2 MB, con licencia MIT, prácticamente el estándar de facto del sector de código abierto. Silero devuelve una probabilidad por fotograma de que la ventana actual de 32 milisegundos contenga habla. En una CPU moderna, el coste es insignificante: unos 0,1 milisegundos por fragmento de 32 milisegundos, o aproximadamente un tercio de un por ciento de un núcleo.
La canalización en vivo ejecuta Silero de forma continua sobre el audio del sistema durante una llamada. Los umbrales con los que realmente se distribuye están ajustados para audio real de reuniones, no para un benchmark limpio:
- Tamaño de ventana: 512 muestras (32 ms a 16 kHz)
- Umbral de habla: 0,15: deliberadamente agresivo para no perder a hablantes silenciosos o lejanos
- Umbral de silencio: 0,08
- El segmento termina tras: 16 ventanas silenciosas consecutivas (~512 ms)
- Segmento mínimo para embedding: 0,8 segundos
- Cadencia de identificación en vivo: cada 1 segundo una vez acumulados ≥1,6 segundos de habla
Compromiso honesto
Un umbral de habla agresivo de 0,15 capta a quienes hablan bajo en micrófonos lejanos, pero también dispara de vez en cuando falsos positivos con pulsaciones de teclado fuertes o ruido de sala. El filtro de segmento mínimo de 0,8 segundos descarta la mayoría de esos falsos positivos antes de que lleguen a la etapa de embedding, de modo que la precisión posterior queda protegida.
Etapa 2: Segmentación (pyannote 3.0)
La segmentación toma el habla continua y la corta en fragmentos que contienen un único hablante cada uno, detectando tanto los puntos de cambio de hablante como las regiones de solapamiento. Es la etapa que decide dónde están los límites entre hablantes. Todo lo posterior asume que esos límites son correctos.
Usamos pyannote-segmentation-3.0, la versión actual del modelo del equipo de pyannote.audio, exportado a ONNX por el proyecto sherpa-onnx . Pyannote es la línea base académica de facto: la canalización de pyannote.audio logra tasas de error de diarización en el rango del 12–15 % en benchmarks estándar como AMI y CALLHOME, las mismas cifras que les gusta citar a los proveedores comerciales en la nube.
¿Por qué la versión ONNX en concreto? Porque incluir PyTorch dentro de una app de Electron es inviable: solo el runtime ocupa unos 1,5 GB y, en la práctica, requiere una GPU para ser rápido. La exportación a ONNX es un único archivo de 6,6 MB que se ejecuta en la CPU mediante ONNX Runtime. Mismo modelo, una fracción minúscula del peso.
Lo invocamos a través del binario de diarización offline de sherpa-onnx con un puñado de flags. Los ajustes de duración mínima provienen de los valores predeterminados recomendados en el artículo de pyannote: las ráfagas cortas de habla se fusionan con el silencio adyacente, y los silencios cortos no rompen un segmento continuo de hablante. La salida es una lista de pares de tiempo inicio/fin etiquetados con IDs de hablante provisionales ( speaker_0, speaker_1, etc.).
Etapa 3: Embeddings de hablante (CAM++)
Un embedding de hablante es una lista de números de longitud fija que representa una voz del mismo modo que un hash representa un archivo. Dos grabaciones de la misma voz producen embeddings que están cerca en el espacio vectorial; voces distintas producen embeddings que están lejos. La «cercanía» se mide mediante similitud del coseno: un único número entre -1 y 1. Esto es el corazón matemático de todo lo demás en la canalización.
Nuestro modelo de embedding es CAM++ del proyecto 3D-Speaker de la DAMO Academy de Alibaba, entrenado con el benchmark de verificación de hablantes VoxCeleb . El archivo concreto que distribuimos es 3dspeaker_speech_campplus_sv_en_voxceleb_16k.onnx, una exportación ONNX de unos 28 MB que produce un vector float32 de 512 dimensiones por segmento. El artículo es arXiv:2303.00332 (Wang et al., 2023).
Elegimos CAM++ frente a la alternativa obvia, ECAPA-TDNN, por tres razones concretas: aproximadamente la mitad de parámetros, menor tasa de error igual (EER) en VoxCeleb1-O e inferencia más rápida en CPU. El modelo MSDD de NVIDIA NeMo no tiene exportación ONNX de primera mano y depende en gran medida de PyTorch. Picovoice Falcon conlleva una licencia comercial por puesto. El framework CoreML Speech de Apple es exclusivo de macOS. CAM++ fue la única opción que satisfizo nuestras restricciones: multiplataforma, ONNX, licencia abierta y rápido en CPU.
Para alimentar a CAM++ distribuimos nuestro propio extractor de características de banco de filtros log-mel en src/helpers/speakerEmbeddings.js: 80 bandas mel, ventanas de 25 milisegundos, saltos de 10 milisegundos, parámetros estándar para esta clase de modelo. Las características fluyen al modelo ONNX mediante onnxruntime-node, ejecutándose en el proceso principal de Electron. Por el otro lado sale un Float32Array de 512 entradas, que escribimos en SQLite como un BLOB de 2048 bytes.
El paso de emparejamiento en sí es lo más simple posible: similitud del coseno entre dos vectores. Este es el código literal que decide si dos segmentos de reunión corresponden al mismo hablante:
// src/helpers/speakerEmbeddings.js
cosineSimilarity(a, b) {
let dot = 0, normA = 0, normB = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
const denom = Math.sqrt(normA) * Math.sqrt(normB);
return denom === 0 ? 0 : dot / denom;
}¿Por qué 512 dimensiones?
512 es el punto óptimo actual en el ranking de verificación de hablantes de VoxCeleb entre precisión y coste de almacenamiento. Las 256 dimensiones empiezan a perder poder discriminativo en voces parecidas pero distintas; las 1024 dimensiones añaden almacenamiento y cómputo con rendimientos decrecientes. 512 float32 × 2 KB × mil reuniones de diez hablantes son 20 MB en disco. Eso cabe cómodamente en un archivo SQLite local.
Etapa 4: Clustering — el problema de «¿cuántos hablantes?»
Después de hacer el embedding de cada segmento tienes N vectores. El clustering los agrupa en K hablantes, pero la parte difícil es que normalmente no conoces K de antemano. Una llamada de ventas puede tener 2 hablantes. Una reunión general puede tener 8. El clustering con K fijo falla en ambos extremos.
Usamos clustering aglomerativo con un umbral de similitud del coseno de 0,5: empieza con cada segmento como su propio clúster, fusiona los dos clústeres con la mayor similitud y repite hasta que ningún par restante supere el umbral. El umbral, no un recuento objetivo, decide dónde detenerse. Esto escala de forma natural de dos hablantes a diez sin ninguna configuración.
La invocación real reside en src/helpers/diarization.js. Es una generación del binario de sherpa-onnx con un puñado de flags de CLI:
// src/helpers/diarization.js
const args = [
`--segmentation.pyannote-model=${segPath}`,
`--embedding.model=${embPath}`,
`--clustering.num-clusters=${numSpeakers}`, // -1 = auto
`--clustering.cluster-threshold=${threshold}`, // 0.5 default
"--min-duration-on=0.2",
"--min-duration-off=0.5",
wavPath,
];La salida es texto plano: una línea por segmento, con el formato start_sec -- end_sec speaker_NN. Lo analizamos en un array de objetos y luego recorremos la transcripción, emparejando cada segmento de la transcripción con el segmento de diarización que tenga el máximo solapamiento temporal. Los segmentos procedentes del micrófono siempre se etiquetan como you: el origen gana a la voz, siempre.
El umbral de 0,5 no es una conjetura. Lo ajustamos frente a nuestro propio banco de evaluación (scripts/meeting-diarization-eval.js) sobre grabaciones reales de reuniones. Umbrales más altos subagrupan: un hablante se divide en dos. Umbrales más bajos sobreagrupan: dos voces parecidas se fusionan. 0,5 es el punto óptimo al que llegamos.
Diarización en vivo: etiquetas mientras hablas
La diarización solo por lotes es precisa pero invisible durante una llamada. La diarización solo en vivo es inmediata pero ruidosa. OpenWhispr ejecuta ambas: las etiquetas en vivo aparecen durante la reunión, y una pasada completa por lotes las refina después.
La ruta en vivo reside en src/helpers/liveSpeakerIdentifier.js. Toma el flujo de audio del sistema a 16 kHz, alimenta cada fotograma de 32 milisegundos a Silero VAD y acumula fotogramas de habla hasta que un segmento se cierra (tras ~512 ms de silencio) o alcanza la cadencia de identificación en vivo (cada 1 segundo una vez acumulados ≥1,6 segundos). Cuando se dispara, extrae un embedding CAM++, lo compara con el mapa en memoria de embeddings de la llamada activa y los perfiles de hablante almacenados, y lanza un evento IPC — meeting-speaker-identified— con la mejor coincidencia. El lado de React lo recoge y actualiza la burbuja de transcripción en el sitio.
Una vez que una persona ha fijado manualmente el nombre de un hablante en la interfaz, esa asignación queda bloqueada. La pasada por lotes posterior a la reunión no puede sobrescribirla, ni siquiera si su propio análisis discrepa. Este comportamiento de bloqueo reside en src/utils/transcriptSpeakerState.ts, y es importante: la peor experiencia de usuario posible es dejar que una pasada automatizada sobrescriba en silencio la corrección de un usuario.
Cuando la grabación se detiene, ejecutamos la diarización offline completa sobre el archivo WAV entero. El procesamiento por lotes tiene el contexto completo —ve toda la reunión, no solo los últimos 1,6 segundos— y produce un etiquetado final más limpio. El resultado por lotes se reconcilia con las etiquetas en vivo, respeta cada bloqueo del usuario y sustituye las conjeturas provisionales por la agrupación más precisa.
Por qué el enfoque híbrido supera a cualquiera por separado
El modo solo en vivo es rápido pero ruidoso en segmentos cortos. El modo solo por lotes es preciso pero silencioso hasta que termina la reunión. El híbrido te da ambas cosas: retroalimentación instantánea durante la llamada y una transcripción final que refleja el análisis con contexto completo. El coste es un paso adicional de reconciliación, que se ejecuta en segundo plano una vez y es prácticamente gratuito.
Perfiles de voz: recordar a los hablantes entre reuniones
Una vez que etiquetas a un hablante como «Alice» en una reunión, OpenWhispr la etiqueta automáticamente en cada reunión futura, sin que hagas nada y sin que su huella de voz salga jamás de tu dispositivo.
El mecanismo es sencillo: el embedding centroide de cada hablante se almacena en tu base de datos SQLite local. Cuando aparecen nuevos embeddings en una llamada futura, los comparamos por similitud del coseno con cada perfil almacenado. Una coincidencia por encima del umbral asigna el nombre automáticamente. Nada de esto toca un servidor. Las tres tablas que lo impulsan son SQLite puro:
-- src/helpers/database.js
CREATE TABLE speaker_profiles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
display_name TEXT NOT NULL,
email TEXT,
embedding BLOB NOT NULL, -- 512 float32s (~2KB)
sample_count INTEGER DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE speaker_mappings (
note_id INTEGER NOT NULL,
speaker_id TEXT NOT NULL,
profile_id INTEGER,
display_name TEXT NOT NULL,
PRIMARY KEY (note_id, speaker_id),
FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE,
FOREIGN KEY (profile_id) REFERENCES speaker_profiles(id) ON DELETE SET NULL
);
CREATE TABLE note_speaker_embeddings (
note_id INTEGER NOT NULL,
speaker_id TEXT NOT NULL,
embedding BLOB NOT NULL,
PRIMARY KEY (note_id, speaker_id),
FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE
);El emparejamiento es de tres niveles, para equilibrar la confianza frente a los falsos positivos:
- Coseno ≥ 0,70: confirmación automática. La etiqueta aparece de inmediato sin ninguna solicitud.
- 0,55 ≤ Coseno < 0,70: sugerencia. La interfaz muestra «¿Es esta Alice?» con controles de confirmar / descartar, y espera tu respuesta.
- Coseno < 0,55: permanece anónimo. El segmento sigue siendo Hablante N hasta que lo nombres manualmente.
Cuando confirmas una coincidencia, el centroide almacenado se actualiza mediante media móvil en lugar de una sobrescritura directa. La fórmula es simple: new_avg = (stored_avg × sample_count + new_embedding) / (sample_count + 1). Esto gestiona la deriva de la voz —una mañana fría, un micrófono distinto, un cable defectuoso— sin romper las coincidencias futuras. Cuantas más muestras de la voz de Alice haya visto el sistema, más estable se vuelve su perfil.
Los perfiles nuevos también desencadenan un reetiquetado retroactivo. Cuando nombras a un hablante por primera vez, una tarea en segundo plano recorre los embeddings almacenados de cada reunión pasada, encuentra coincidencias y actualiza a los hablantes previamente sin nombre en las transcripciones históricas. Las asignaciones bloqueadas por el usuario nunca se tocan. El resultado: nombrar a Alice una sola vez la etiqueta retroactivamente en todas las llamadas anteriores a las que asistió.
Si Google Calendar está conectado y la reunión tiene asistentes, la interfaz de selección de hablante se rellena previamente con sus nombres y correos. Un clic asigna Hablante 2 → alice@example.comy persiste para siempre: los asistentes y los perfiles de voz quedan vinculados, de modo que los nombres correctos aparecen incluso entre dispositivos.
Dónde residen realmente tus datos
El audio nunca sale de tu dispositivo. Eso no es un eslogan de marketing: es una propiedad mecánica del código y, como OpenWhispr es de código abierto, puedes verificarlo tú mismo. Esto es exactamente lo que le ocurre a cada dato en cada paso.
| Qué | Dónde reside | ¿Sale de tu dispositivo? |
|---|---|---|
| Audio bruto de la reunión | Directorio temporal del SO, eliminado tras la diarización | No — nunca |
| Modelos ONNX de diarización | ~/.cache/openwhispr/diarization-models/ | Se descargan una vez en el primer inicio, y luego nunca más |
| Embeddings de hablante (huellas de voz) | BLOB de SQLite local | No — nunca |
| Asignaciones de hablante a nombre | Tabla de SQLite local | No — nunca |
| Texto de la transcripción | SQLite local (sincronización opcional con la nube si la activas) | Solo si lo activas |
El audio PCM bruto de una reunión se escribe en un archivo del directorio temporal de tu SO, se entrega al binario de sherpa-onnx generado localmente y se elimina en un bloque finallycuando la diarización termina. No existe ninguna ruta de código en la canalización de diarización de OpenWhispr que abra un socket de red para subir audio. Como el código es de código abierto, puedes hacer grep en src/helpers/diarization.js, src/helpers/liveSpeakerIdentifier.js, y src/helpers/speakerEmbeddings.js buscando fetch, axios, o http y confirmarlo tú mismo.
La IPC entre los procesos principal y de renderizado de Electron utiliza el contextBridge integrado: una tubería a nivel de kernel dentro de un único proceso, no un socket de red. Los embeddings de hablante y los perfiles de voz se almacenan como BLOB de SQLite en el mismo archivo de base de datos que tus notas, con los mismos permisos de archivo a nivel de SO.
Una excepción, con total transparencia
La única actividad de red en esta función es la descarga única del modelo en el primer inicio: tres archivos ONNX obtenidos de la CDN de publicaciones de sherpa-onnx en GitHub. Lo documentamos explícitamente porque «una llamada de red durante la configuración, cero después» es una historia de privacidad muy distinta de «inferencia en la nube siempre activa» y, como asistente de reuniones de código abierto, preferimos decírtelo a que lo descubras por tu cuenta.
Limitaciones honestas
La diarización local es buena, no perfecta. Estos son los casos en los que de verdad le cuesta, y qué hacemos con cada uno.
- Hablantes solapados. Dos personas hablando a la vez es intrínsecamente con pérdida para la atribución de una sola etiqueta. Pyannote 3.0 detecta la región de solapamiento, pero un único speaker_id por segmento no basta para representar «las dos personas hablando». Marcamos esos segmentos como provisionales y te dejamos corregirlos en la interfaz.
- Precisión de arranque en frío. La primera reunión con un colega nuevo usa únicamente las características genéricas entrenadas con VoxCeleb. Una vez que lo etiquetas una vez y el perfil tiene unas pocas muestras, las reuniones posteriores son notablemente más precisas.
- Intervenciones cortas. «Sí», «vale», una risa rápida: cualquier cosa de menos de ~0,8 segundos no puede producir un embedding fiable. Descartamos esos segmentos de la etapa de embedding y recurrimos a la propagación de etiquetas desde el contexto circundante.
- Audio de campo lejano o con bajo SNR. Alguien en el altavoz de un teléfono en una sala ruidosa se degradará con elegancia, pero de forma visible. El control automático de ganancia y el ajuste de la VAD ayudan, pero ninguna sofisticación del modelo supera por completo una mala física.
- Inferencia solo en CPU. Una GPU sería más rápida, pero deliberadamente no la exigimos. En la práctica, el rendimiento en una CPU moderna es bueno: ~100 milisegundos por embedding y unos 30 segundos de diarización por lotes para una reunión de 45 minutos en un Mac M1.
Publicamos nuestro banco de evaluación ( (scripts/meeting-diarization-eval.js) ) en el repositorio de código abierto para que puedas medir cualquiera de estos aspectos tú mismo, con tus propias grabaciones, no con nuestros clips de demostración seleccionados a conveniencia. Eso nos parece más honesto que una cifra reluciente en una presentación de marketing.
Por qué elegimos Sherpa-ONNX
Necesitábamos un runtime de diarización que fuera multiplataforma, libre de Python, rápido en CPU y de código abierto. Sherpa-ONNX fue la única opción que cumplía las cuatro condiciones.
Nuestros requisitos eran concretos. Tenía que ejecutarse en macOS (tanto Intel como Apple Silicon), Windows x64 y Linux x64 sin rutas de código separadas. No podía incluir PyTorch: solo el runtime ocupa unos 1,5 GB y, en esencia, requiere una GPU para ser utilizable a latencias interactivas. No podía requerir CUDA. Y la licencia tenía que ser lo bastante permisiva como para distribuirse en una app de escritorio comercial pero de código abierto.
Esto es lo que realmente evaluamos:
- pyannote.audio (Python): descartado. Incluir PyTorch en una app de Electron es inviable.
- NVIDIA NeMo MSDD: dependiente de PyTorch, sin exportación ONNX de primera mano, centrado en la GPU.
- Picovoice Falcon: licencia comercial por usuario activo más activación en la nube en el primer arranque.
- Apple CoreML + framework Speech: exclusivo de macOS, sin paridad multiplataforma.
- sherpa-onnx: Apache 2.0, binario ONNX nativo, ~35 MB de modelos, interfaz de generar y leer stdout, mantenido activamente por k2-fsa. La misma cadena de herramientas impulsa ASR en producción en otros productos ya en el mercado.
Una salvedad honesta: el binario de diarización de hablantes offline de sherpa-onnx es relativamente nuevo. Fijamos la versión a v1.12.23y ejecutamos nuestro banco de evaluación completo frente a cada actualización antes de subir la versión fijada. Ese es el compromiso correcto para algo tan crítico.
Preguntas frecuentes
- ¿Existe un tomador de notas de reuniones privado que no suba mi audio a la nube?
- Sí. OpenWhispr es un tomador de notas de reuniones gratuito y de código abierto donde tu audio nunca sale de tu dispositivo. La transcripción se ejecuta en local con OpenAI Whisper o NVIDIA Parakeet, la diarización de hablantes se ejecuta en local con segmentación pyannote y embeddings CAM++, y las huellas de voz se almacenan en un archivo SQLite local en tu propia máquina, no en nuestros servidores.
- ¿Pueden Otter, Fireflies o Granola ver mis reuniones?
- Sí. Los tomadores de notas con IA basados en la nube suben el audio de tu reunión a sus servidores para transcribirlo y diarizarlo. Las etiquetas de hablante, las transcripciones y los embeddings de voz se calculan en su infraestructura y se almacenan en sus bases de datos. Si necesitas una alternativa que mantenga cada byte de audio en tu dispositivo, necesitas una herramienta local-first como OpenWhispr.
- ¿Qué es la diarización de hablantes?
- La diarización de hablantes es el proceso de etiquetar quién habló y cuándo en una grabación, a diferencia de la transcripción, que responde a qué se dijo. Una transcripción diarizada muestra cada intervención etiquetada con un hablante —Alice, Bob, Hablante 3— en lugar de un muro de texto indiferenciado.
- ¿La diarización local de hablantes funciona de verdad, o es una rebaja respecto a la nube?
- Funciona. La canalización de OpenWhispr —pyannote-segmentation-3.0 más embeddings CAM++ del proyecto 3D-Speaker, ejecutada vía sherpa-onnx— logra tasas de error de diarización en el mismo orden de magnitud que las API comerciales en la nube en benchmarks estándar. Los principales compromisos honestos son el manejo de hablantes solapados y la precisión de arranque en frío con voces que el sistema nunca ha oído antes.
- ¿Cuánto espacio en disco usan los modelos de diarización local?
- Unos 45 MB en total: segmentación pyannote (6,6 MB) más el modelo de embedding de hablante CAM++ (28 MB) más Silero VAD (2 MB). Los tres se descargan una vez en el primer inicio y se almacenan en caché en ~/.cache/openwhispr/diarization-models/. Después de eso permanecen en disco y nunca se vuelven a descargar.
- ¿Necesito una GPU para ejecutar la diarización local de OpenWhispr?
- No. Todo se ejecuta en la CPU mediante ONNX Runtime. Los Apple Silicon modernos y las CPU x86 recientes lo gestionan con holgura: en la práctica, unos 30 segundos de diarización por lotes para una reunión de 45 minutos en un Mac M1, con etiquetas de hablante en vivo que aparecen en uno o dos segundos durante la propia llamada.
- ¿OpenWhispr es realmente de código abierto?
- Sí. El código de diarización, la canalización de transcripción y el resto de la app de escritorio son de código abierto en GitHub. Las afirmaciones de privacidad de este artículo son directamente verificables: haz grep en los ayudantes de diarización buscando fetch, axios o http y no encontrarás ninguna llamada de red en la ruta de diarización.
- ¿Qué ocurre cuando dos personas hablan a la vez en una llamada?
- Pyannote 3.0 detecta y anota explícitamente las regiones de solapamiento, pero la atribución de una sola etiqueta durante el solapamiento es intrínsecamente con pérdida. Marcamos esos segmentos como provisionales en la transcripción y te dejamos corregirlos haciendo clic en la etiqueta de hablante. Tus correcciones manuales quedan bloqueadas y nunca las sobrescriben pasadas automatizadas posteriores.
Prueba el tomador de notas de reuniones privado
OpenWhispr es un asistente de reuniones gratuito y de código abierto donde tu audio nunca sale de tu dispositivo. Diarización local, transcripción local, perfiles de voz locales. Funciona en macOS, Windows y Linux.
Sin necesidad de cuenta · Funciona sin conexión · Código abierto para siempre