Transcripción de voz on-device en iOS: WhisperKit vs SFSpeechRecognizer vs SpeechAnalyzer#
Cuando diseñé la capa de reconocimiento de voz para Lanternly —un diario que estoy desarrollando actualmente—, el requisito era simple y a la vez inflexible: una nota de voz se convierte en texto directamente en el teléfono, y ningún byte de audio puede salir del dispositivo. Un diario es una de las categorías de datos más personales que existen, y "todo se cifra en el servidor" no suena convincente aquí, ni para mí ni para el usuario. La única respuesta honesta es no tener servidor para esta operación en absoluto.
En los últimos años han aparecido tres formas realistas de hacerlo en iOS: el veterano SFSpeechRecognizer con su bandera on-device, el motor de terceros WhisperKit de Argmax construido sobre los modelos Whisper, y el flamante SpeechAnalyzer de iOS 26. En este artículo —sin marketing, con números y código— analizo en qué se diferencian y cómo elegí entre ellos.
Por qué la transcripción on-device importa#
La transcripción en la nube es el camino más simple: envías el audio a una API, recibes texto, siguiente línea de código. Pero tiene un costo que a los desarrolladores les gusta callar. Primero, la privacidad: la voz llega al servidor de otro, y sin importar lo que diga la política de privacidad del proveedor, legal y técnicamente ya no es "solo en tu dispositivo". Segundo, la dependencia de red: una nota de voz grabada en el metro sin señal simplemente no se transcribe. Tercero, el costo: cada minuto de audio a través de una API en la nube es una factura que crece junto con tu base de usuarios.
Para una app como un diario, el primer punto no es negociable en absoluto: no es una preferencia técnica, es una condición para que el producto exista. Así que la pregunta no era "local o nube", sino "qué motor local exactamente".
Tres motores sobre la mesa#
SFSpeechRecognizer — parte del framework Speech, disponible desde iOS 10. Tiene una bandera requiresOnDeviceRecognition que obliga al reconocimiento a funcionar totalmente en el dispositivo, sin contactar los servidores de Apple. El modelo ya viene integrado en el sistema para los idiomas soportados; el desarrollador no necesita descargar nada.
WhisperKit — un paquete Swift de código abierto de Argmax que porta los modelos Whisper de OpenAI a Core ML y al Apple Neural Engine. En 2026, Argmax fusionó WhisperKit con SpeakerKit (diarización) y TTSKit en un único SDK open-source, pero el núcleo de transcripción se mantuvo igual: tú eliges y descargas el tamaño de modelo que necesitas, y WhisperKit lo ejecuta localmente con optimización específica para el ANE.
SpeechAnalyzer / SpeechTranscriber — una nueva API modular del framework Speech, introducida en iOS 26. No es una actualización cosmética de SFSpeechRecognizer, sino una arquitectura distinta: una API asíncrona construida sobre AsyncSequence, con módulos separados —SpeechTranscriber (habla larga), DictationTranscriber (frases cortas, equivalente al recognizer anterior) y SpeechDetector (detección de actividad de voz).
WhisperKit: qué hay bajo el capó#
WhisperKit no entrena su propio modelo: toma los pesos ya entrenados de Whisper (tiny, base, small, medium, large-v3 y el compacto large-v3-turbo) y los convierte a Core ML, distribuyendo el cómputo entre CPU, GPU y Neural Engine. La diferencia de peso entre modelos es enorme: tiny ocupa unos 40 MB y sirve sobre todo para depuración, mientras que large-v3 ronda 1.5 GB. El compromiso práctico habitual en producción en iPhone 13 y modelos posteriores es large-v3-turbo: precisión casi idéntica a large-v3 completo con alrededor de cinco veces más rendimiento, en unos 600 MB en disco.
La ventaja de ingeniería clave de WhisperKit no es el modelo Whisper en sí (es abierto y cualquiera podría integrarlo), sino la optimización para el ANE: benchmarks independientes muestran una aceleración extra de 1.3–1.8x sobre una ejecución simple con Metal, especialmente notable en los chips de la línea M3/M4. En iPhone 13 y modelos posteriores esto permite transmitir la transcripción en tiempo real en lugar de esperar a que termine la grabación.
La gran ventaja de Whisper como modelo es el multilingüismo de fábrica: está entrenado en 99 idiomas simultáneamente, incluido el ruso, y maneja de forma natural el cambio de idioma dentro de una misma frase —"la reunión fue en ruso, con un término y medio en inglés" es el pan de cada día para Whisper, no un caso límite.
SpeechAnalyzer: la nueva carta de Apple#
SpeechAnalyzer no es solo otra API más: es la respuesta de Apple al hecho de que modelos de propósito general como Whisper superaban en calidad al recognizer del sistema. Y la respuesta es contundente: en la porción limpia del dataset LibriSpeech, SpeechAnalyzer alcanza una tasa de error de palabra (WER) de alrededor de 2.12%, y de alrededor de 4.56% en la porción ruidosa. Como comparación, Whisper Small obtiene alrededor de 3.74% en la misma porción limpia, es decir, en inglés el nuevo motor de sistema de Apple supera objetivamente a un modelo abierto de tamaño comparable.
Pero esta victoria trae una limitación importante que conviene decir con honestidad: a la salida de iOS 26, los idiomas soportados por SpeechTranscriber cubren sobre todo los idiomas en los que Apple ya invertía para Siri —inglés, español, francés, alemán, italiano, portugués, chino, japonés, coreano. El ruso no está en esa lista. Para una app que necesita transcripción en ruso (como Lanternly), eso descarta SpeechAnalyzer de inmediato, por muy impresionante que sea su precisión en inglés.
Otro detalle arquitectónico: los modelos de idioma de SpeechAnalyzer no van incrustados en el binario de tu app, sino que el sistema los gestiona mediante AssetInventory y se descargan bajo demanda. Eso reduce el tamaño de tu app bundle, pero añade una dependencia: que el asset de idioma correcto exista y esté disponible en el dispositivo del usuario.
Una comparación honesta#
La tabla siguiente no es "qué motor es mejor", sino qué motor es mejor para qué tarea. Los tres tienen su lugar en distintos escenarios.
| Criterio | SFSpeechRecognizer | WhisperKit | SpeechAnalyzer (iOS 26+) |
|---|---|---|---|
| Precisión | Media, depende del idioma | Alta, robusta ante ruido y acento | La mejor en idiomas soportados (WER ~2.1% en habla limpia) |
| Idiomas | Lista de locale limitada, pero incluye ru-RU | 99 idiomas, incluido el ruso, code-switching libre | ~9 idiomas, sin ruso en el lanzamiento de iOS 26 |
| Offline | Sí, con requiresOnDeviceRecognition y modelo local presente | Sí, totalmente, el modelo se descarga una vez | Sí, assets del sistema vía AssetInventory |
| Peso para la app | 0 MB — el modelo ya está en el sistema | Desde ~40 MB (tiny) hasta ~1.5 GB (large-v3), ~600 MB (turbo) en la práctica | 0 MB en el binario, pero requiere un asset del SO para el idioma |
| iOS mínimo | iOS 10+ (on-device — iOS 13+) | iOS 16+ (según el modelo) | iOS 26+ |
En la práctica: cómo se ve en código#
A continuación, un esqueleto básico de integración con WhisperKit: inicialización, selección de modelo según la memoria del dispositivo, transcripción de un archivo ya grabado y streaming durante la grabación. Es código ilustrativo pero fiel en espíritu: exactamente el punto donde diseñé que el motor de Lanternly pudiera intercambiarse, sin tocar el código que lo llama.
import WhisperKit
final class LocalTranscriber {
private var pipeline: WhisperKit?
/// Initializes WhisperKit, downloading and warming up the model
func setUp() async throws {
let config = WhisperKitConfig(
model: "large-v3-turbo", // compact yet accurate option
downloadBase: nil, // model is pulled from the Hugging Face Hub
verbose: false,
logLevel: .none,
prewarm: true, // warm up the Neural Engine ahead of time
load: true
)
pipeline = try await WhisperKit(config)
}
}extension LocalTranscriber {
/// Picks a model based on the device's available memory
static func recommendedModel() -> String {
let physicalMemory = ProcessInfo.processInfo.physicalMemory
let memoryGB = Double(physicalMemory) / 1_073_741_824
switch memoryGB {
case ..<4:
return "tiny" // ~40 MB, for older devices
case 4..<6:
return "base" // ~150 MB, speed/accuracy balance
case 6..<8:
return "small" // ~500 MB
default:
return "large-v3-turbo" // ~600 MB, best balance on the Neural Engine
}
}
}extension LocalTranscriber {
/// Transcribes an already-recorded audio file (m4a/wav) entirely on-device
func transcribe(fileAt url: URL) async throws -> String {
guard let pipeline else {
throw TranscriptionError.notReady
}
let options = DecodingOptions(
language: "ru", // a hint to the model, not a hard constraint
temperature: 0.0,
withoutTimestamps: true
)
let results = try await pipeline.transcribe(
audioPath: url.path,
decodeOptions: options
)
return results?.text ?? ""
}
}
enum TranscriptionError: Error {
case notReady
}extension LocalTranscriber {
/// Streaming transcription while a voice note is being recorded
func startStreaming(onPartialResult: @escaping (String) -> Void) async throws {
guard let pipeline else {
throw TranscriptionError.notReady
}
let streamer = AudioStreamTranscriber(
audioEncoder: pipeline.audioEncoder,
featureExtractor: pipeline.featureExtractor,
segmentSeeker: pipeline.segmentSeeker,
textDecoder: pipeline.textDecoder,
tokenizer: pipeline.tokenizer!,
audioProcessor: pipeline.audioProcessor,
decodingOptions: DecodingOptions(language: "ru")
) { _, result in
onPartialResult(result.text)
}
try await streamer.startStreamTranscription()
}
}Qué elegiría yo#
Si el ruso no fuera un requisito y iOS 26 pudiera ser la versión mínima, elegiría SpeechAnalyzer sin dudarlo: mayor precisión, sin inflar el binario, y una API diseñada para la concurrencia estructurada moderna de Swift. Pero la realidad de la mayoría de los productos es soporte para el ruso y una versión mínima de iOS por debajo de 26, lo que significa que la elección real es entre SFSpeechRecognizer y WhisperKit.
SFSpeechRecognizer con requiresOnDeviceRecognition es una opción honesta y prácticamente gratuita: ya está en el sistema, no requiere descargar ningún modelo y funciona muy bien como línea base. Eso es exactamente lo que impulsa la transcripción en Lanternly hoy: diseñé deliberadamente la interfaz para que el motor pueda intercambiarse en un único punto del código, sin cambios más arriba en la pila. WhisperKit es el siguiente paso, para cuando necesitas mayor robustez ante ruido, acento y habla mixta, y unos cientos de megabytes extra son un precio aceptable por esa calidad.
Checklist antes de elegir un motor#
- ¿Necesitas estrictamente modo offline sin ninguna petición al servidor? Si es así, cualquier opción "en la nube con caché" queda descartada de inmediato
- ¿Tu idioma principal está soportado por
SpeechAnalyzeren tu versión mínima objetivo de iOS? - ¿Estás dispuesto a añadir entre 150 y 600 MB a tu app por un modelo de WhisperKit?
- ¿Necesitas code-switching (mezcla de idiomas en una misma frase)? Entonces WhisperKit es la respuesta clara
- ¿Es suficiente la precisión integrada de
SFSpeechRecognizerpara tu escenario, o los usuarios se quejan del reconocimiento?
Conclusión#
No existe una respuesta universal a "qué motor es mejor": hay tres puntos honestos en la curva de compromiso entre precisión, peso y cobertura de idiomas. SpeechAnalyzer gana en precisión pura donde su soporte de idiomas alcanza. WhisperKit sigue siendo la opción más flexible para escenarios multilingües y con acento, al costo del peso del modelo. Y SFSpeechRecognizer es el caballo de batalla que casi no cuesta nada y a menudo es suficiente. Al diseñar una capa así, la decisión más importante no es qué motor concreto eliges, sino la arquitectura que te permite reemplazarlo más adelante sin reescribir la aplicación.



