En la era de la inteligencia artificial generativa, los asistentes de voz han dejado de ser herramientas rígidas basadas en comandos preprogramados para convertirse en compañeros de conversación fluidos e inteligentes. Aunque Siri es el asistente nativo de Apple, la flexibilidad que ofrece la API de OpenAI nos permite diseñar un asistente de voz personalizado, ultra inteligente y adaptado a necesidades específicas.
En este tutorial paso a paso, aprenderás cómo crear tu propio asistente de voz nativo en iOS utilizando Swift, SwiftUI y la API de OpenAI.
¿Cómo funciona un Asistente de Voz en iOS?
Para crear un asistente de voz eficiente, debemos conectar tres pilares tecnológicos fundamentales en un flujo de trabajo asíncrono:
- Reconocimiento de Voz (Speech-to-Text): Convertir la voz del usuario en texto utilizando el framework nativo de Apple
Speech. - Procesamiento de Lenguaje Natural (IA): Enviar ese texto a la API de OpenAI (usando modelos como GPT-4o o GPT-3.5) para obtener una respuesta coherente.
- Síntesis de Voz (Text-to-Speech): Convertir la respuesta de texto de OpenAI de nuevo en audio hablado utilizando el framework
AVFoundation.
Requisitos Previos
Antes de comenzar a escribir código, asegúrate de contar con lo siguiente:
- Un Mac con Xcode 15 o superior instalado.
- Un dispositivo iOS físico (es necesario para probar el reconocimiento de voz y el micrófono de forma óptima).
- Una cuenta de desarrollador de OpenAI y una API Key activa.
- Conocimientos básicos de Swift y SwiftUI.
Paso 1: Configurar los Permisos en el Info.plist
El acceso al micrófono y el reconocimiento de voz son datos sensibles de privacidad en iOS. Por lo tanto, debes solicitar permiso explícito al usuario.
Abre tu proyecto en Xcode, ve al archivo Info.plist (o la pestaña Info del target de tu app) y añade las siguientes dos claves con sus respectivas descripciones explicativas:
- Privacy – Microphone Usage Description: «Necesitamos acceso a tu micrófono para capturar tus comandos de voz.»
- Privacy – Speech Recognition Usage Description: «Utilizamos el reconocimiento de voz de Apple para transcribir lo que dices al asistente.»
Paso 2: Crear el Administrador de Reconocimiento de Voz (Speech-to-Text)
Vamos a crear una clase encargada de escuchar al usuario y transcribir sus palabras en tiempo real. Utilizaremos el framework Speech de Apple.
Crea un archivo llamado SpeechManager.swift y añade el siguiente código:
import Foundation
import Speech
import AVFoundation
class SpeechManager: ObservableObject {
private let audioEngine = AVAudioEngine()
private let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "es-ES"))
private var recognitionRequest: SFSpeechAudioBufferRecognitionRequest?
private var recognitionTask: SFSpeechRecognitionTask?
@Published var transcribedText: String = ""
@Published var isRecording: Bool = false
func checkPermissions(completion: @escaping (Bool) -> Void) {
SFSpeechRecognizer.requestAuthorization { status in
DispatchQueue.main.async {
completion(status == .authorized)
}
}
}
func startRecording() throws {
// Cancelar cualquier tarea previa
recognitionTask?.cancel()
self.recognitionTask = nil
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.record, mode: .measurement, options: .duckOthers)
try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
recognitionRequest = SFSpeechAudioBufferRecognitionRequest()
guard let recognitionRequest = recognitionRequest else { return }
recognitionRequest.shouldReportPartialResults = true
let inputNode = audioEngine.inputNode
let recordingFormat = inputNode.outputFormat(forBus: 0)
inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { buffer, _ in
recognitionRequest.append(buffer)
}
audioEngine.prepare()
try audioEngine.start()
isRecording = true
recognitionTask = speechRecognizer?.recognitionTask(with: recognitionRequest) { result, error in
if let result = result {
DispatchQueue.main.async {
self.transcribedText = result.bestTranscription.formattedString
}
}
if error != nil || result?.isFinal == true {
self.stopRecording()
}
}
}
func stopRecording() {
audioEngine.stop()
audioEngine.inputNode.removeTap(onBus: 0)
recognitionRequest?.endAudio()
isRecording = false
}
}
Paso 3: Integrar la API de OpenAI en Swift
Para comunicarnos con OpenAI sin depender de librerías de terceros complejas, crearemos un servicio de red nativo utilizando URLSession y el nuevo modelo de concurrencia async/await de Swift.
Crea un archivo llamado OpenAIService.swift:
import Foundation
struct ChatResponse: Decodable {
let choices: [Choice]
}
struct Choice: Decodable {
let message: Message
}
struct Message: Codable {
let role: String
let content: String
}
class OpenAIService {
private let apiKey = "TU_API_KEY_AQUÍ" // Reemplaza con tu API Key real
func sendPrompt(prompt: String) async throws -> String {
guard let url = URL(string: "https://api.openai.com/v1/chat/completions") else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
let messages = [
Message(role: "system", content: "Eres un asistente de voz útil y conciso. Responde en español de forma breve."),
Message(role: "user", content: prompt)
]
let parameters: [String: Any] = [
"model": "gpt-4o", // O "gpt-3.5-turbo" si prefieres menor costo
"messages": messages.map { ["role": $0.role, "content": $0.content] }
]
request.httpBody = try JSONSerialization.data(withJSONObject: parameters)
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
let decodedResponse = try JSONDecoder().decode(ChatResponse.self, from: data)
return decodedResponse.choices.first?.message.content ?? "No recibí respuesta."
}
}
Paso 4: Implementar la Síntesis de Voz (Text-to-Speech)
Una vez que OpenAI nos devuelve la respuesta en formato texto, necesitamos que nuestro iPhone la reproduzca de manera natural. Para ello utilizaremos AVSpeechSynthesizer.
Crea un archivo llamado TTSManager.swift:
import AVFoundation
class TTSManager: NSObject, AVSpeechSynthesizerDelegate {
private let synthesizer = AVSpeechSynthesizer()
override init() {
super.init()
synthesizer.delegate = self
}
func speak(text: String) {
// Detener cualquier audio previo antes de hablar
if synthesizer.isSpeaking {
synthesizer.stopSpeaking(at: .immediate)
}
let utterance = AVSpeechUtterance(string: text)
utterance.voice = AVSpeechSynthesisVoice(language: "es-ES")
utterance.rate = AVSpeechUtteranceDefaultSpeechRate // Puedes ajustar la velocidad aquí
// Asegurar que el audio se escuche incluso en modo silencioso
do {
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default, options: .defaultToSpeaker)
try AVAudioSession.sharedInstance().setActive(true)
} catch {
print("Error al configurar la sesión de audio TTS: \(error)")
}
synthesizer.speak(utterance)
}
}
Paso 5: Uniendo todo en la Interfaz de Usuario con SwiftUI
Ahora crearemos una interfaz de usuario atractiva y minimalista en SwiftUI para interactuar con nuestro asistente.
Reemplaza el contenido de tu ContentView.swift por el siguiente código:
import SwiftUI
struct ContentView: View {
@StateObject private var speechManager = SpeechManager()
private let openAIService = OpenAIService()
private let ttsManager = TTSManager()
@State private var assistantResponse: String = "Presiona el micrófono y empieza a hablar."
@State private var isProcessing: Bool = false
var body: some View {
VStack(spacing: 30) {
Text("Asistente de Voz IA")
.font(.largeTitle)
.bold()
.padding(.top)
Spacer()
// Visualización del estado y texto transcribiéndose en tiempo real
VStack(spacing: 15) {
Text(speechManager.isRecording ? "Escuchando..." : (isProcessing ? "Procesando..." : "Listo"))
.font(.subheadline)
.foregroundColor(.gray)
.textCase(.uppercase)
Text(speechManager.transcribedText)
.font(.body)
.multilineTextAlignment(.center)
.italic()
.padding()
.frame(height: 100)
}
Divider()
// Respuesta del Asistente
ScrollView {
Text(assistantResponse)
.font(.title3)
.fontWeight(.medium)
.multilineTextAlignment(.center)
.padding()
}
Spacer()
// Botón de interacción
Button(action: {
toggleRecording()
}) {
Image(systemName: speechManager.isRecording ? "stop.circle.fill" : "mic.circle.fill")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 80, height: 80)
.foregroundColor(speechManager.isRecording ? .red : .blue)
.shadow(radius: 10)
}
.padding(.bottom, 40)
}
.onAppear {
speechManager.checkPermissions { allowed in
if !allowed {
assistantResponse = "Por favor, habilita los permisos de micrófono y voz en Ajustes."
}
}
}
}
private func toggleRecording() {
if speechManager.isRecording {
speechManager.stopRecording()
processUserPrompt()
} else {
speechManager.transcribedText = ""
assistantResponse = "Escuchando tu petición..."
do {
try speechManager.startRecording()
} catch {
assistantResponse = "Error al iniciar grabación: \(error.localizedDescription)"
}
}
}
private func processUserPrompt() {
let prompt = speechManager.transcribedText
guard !prompt.isEmpty else { return }
isProcessing = true
assistantResponse = "Pensando..."
Task {
do {
let response = try await openAIService.sendPrompt(prompt: prompt)
await MainActor.run {
self.assistantResponse = response
self.isProcessing = false
// Hacer que el asistente hable la respuesta
self.ttsManager.speak(text: response)
}
} catch {
await MainActor.run {
self.assistantResponse = "Error de conexión: \(error.localizedDescription)"
self.isProcessing = false
}
}
}
}
}
Buenas Prácticas y Consejos para Producción
Si decides publicar una aplicación basada en este tutorial en el App Store, ten en cuenta las siguientes recomendaciones:
- Seguridad de las API Keys: Nunca subas tu clave de OpenAI directamente en el código de tu app móvil. Un usuario avanzado podría extraerla. En su lugar, utiliza un servidor intermediario (Backend) que procese las solicitudes de forma segura.
- Gestión de Costos: Ajusta los parámetros del payload enviados a OpenAI, como
max_tokens, para evitar facturas elevadas por respuestas demasiado largas. - Experiencia de Usuario (UX): Agrega animaciones de ondas de audio (Lottie o Canvas en SwiftUI) mientras el usuario habla o cuando la IA procesa para mantener una experiencia interactiva óptima.
Conclusión
Integrar herramientas de inteligencia artificial avanzadas como OpenAI con las capacidades nativas de accesibilidad de iOS (Speech y AVFoundation) abre un abanico infinito de posibilidades para crear aplicaciones dinámicas y accesibles.
Con este tutorial, has aprendido los fundamentos de la conversión de voz a texto, el consumo de APIs de IA generativa en Swift y la síntesis de voz final. Ahora es tu turno: ¿Qué funciones innovadoras le añadirás a tu asistente de voz personalizado?