Tutorial: Cómo crear un Asistente de Voz nativo en iOS con OpenAI y Swift

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:

  1. Reconocimiento de Voz (Speech-to-Text): Convertir la voz del usuario en texto utilizando el framework nativo de Apple Speech.
  2. 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.
  3. 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?

Deja una respuesta