Guía Maestra: API Texto a Voz de Google

Integración completa con PHP, cURL, Bootstrap y MySQL para aplicaciones web dinámicas y accesibles.

Probar voces

💡 Introducción Teórica

La API Cloud Text-to-Speech de Google es un servicio que convierte texto escrito en audio con sonido natural. Utiliza la investigación de DeepMind en síntesis de voz (como WaveNet) para ofrecer una amplia gama de voces (+380) en más de 50 idiomas y variantes. Esto permite a los desarrolladores crear aplicaciones que "hablan" a los usuarios, mejorando la accesibilidad y la experiencia de usuario en general.

Casos de uso comunes:

  • Accesibilidad: Leer contenido de sitios web para usuarios con discapacidad visual.
  • Asistentes de voz: Dar respuestas audibles en chatbots y asistentes virtuales.
  • E-learning: Convertir materiales de estudio en lecciones de audio.
  • Sistemas de respuesta de voz interactiva (IVR): En centros de llamadas.
  • Notificaciones: Generar alertas de audio en sistemas de monitoreo.

En esta guía, nos enfocaremos en la interacción directa con la API REST a través de PHP y cURL. Este método no requiere el SDK de Google Cloud y ofrece un control total sobre la solicitud y la respuesta, lo cual es ideal para entornos de hosting compartido donde la instalación de dependencias puede ser limitada.

🔑 Paso 1: Obtener Credenciales y Configurar el Entorno

Para usar la API, necesitas un proyecto de Google Cloud y una clave de API.

  1. Crear un Proyecto en Google Cloud:
    • Ve a la Consola de Google Cloud.
    • Crea un nuevo proyecto (o selecciona uno existente). Dale un nombre descriptivo, como "MiProyecto-TextoAVoz".
  2. Habilitar la API Text-to-Speech:
    • En el menú de navegación, ve a APIs y servicios > Biblioteca.
    • Busca "Cloud Text-to-Speech API" y haz clic en Habilitar.
  3. Habilitar Facturación:
    • Es un requisito, aunque la API tiene un nivel de uso gratuito generoso. Ve a Facturación y asocia una cuenta de facturación a tu proyecto.
    • Precios: Google ofrece una cantidad de caracteres gratuitos al mes para voces estándar y WaveNet. Consulta la página de precios oficial para obtener detalles actualizados.
  4. Crear una Clave de API:
    • Ve a APIs y servicios > Credenciales.
    • Haz clic en + Crear credenciales y selecciona Clave de API.
    • Copia la clave generada. ¡Trátala como una contraseña!
    • Recomendación de seguridad: Haz clic en la clave recién creada y en "Restringir clave". En "Restricciones de API", selecciona "Restringir clave" y elige "Cloud Text-to-Speech API" de la lista. Esto asegura que la clave solo pueda usarse para este servicio.

🔒 ¡Seguridad Primero!

Nunca insertes tu clave de API directamente en el código PHP o JavaScript que es accesible desde el navegador. La mejor práctica es almacenarla en una variable de entorno en tu servidor o en un archivo de configuración fuera del directorio raíz público (`public_html` o `www`).

⚙️ Paso 2: El Corazón del Sistema (Backend con PHP)

Crearemos un script PHP (`generar_audio.php`) que recibirá el texto y los parámetros de la voz, se comunicará con la API de Google y devolverá el audio codificado en Base64.

<?php
// ======================================================================
// ARCHIVO: generar_audio.php
// ROL: Endpoint para convertir texto a voz usando la API de Google.
// ======================================================================

// Directiva #5: Encabezado de Archivo
// error_reporting(0); // Descomentar en producción
// Suponiendo que la conexión ya está en la variable $mysqli según tus directrices.
include("../include/conecta_mysql_otec.php");

header('Content-Type: application/json');

// --- SECCIÓN 1: CONFIGURACIÓN Y SEGURIDAD ---

// ¡IMPORTANTE! Carga la API Key de forma segura.
// NO la escribas directamente aquí. Usa variables de entorno o un include seguro.
// Ejemplo: $config = require '/ruta/segura/config.php'; $apiKey = $config['google_api_key'];
// Para este ejemplo, la definimos aquí, pero NO es la práctica recomendada.
define('GOOGLE_TTS_API_KEY', 'TU_API_KEY_AQUI');

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405); // Método no permitido
    echo json_encode(['error' => 'Método no permitido. Usa POST.']);
    exit;
}

// --- SECCIÓN 2: RECEPCIÓN Y VALIDACIÓN DE DATOS ---

$json_data = file_get_contents('php://input');
$data = json_decode($json_data, true);

// Sanitizar la entrada para prevenir XSS básico si se fuera a mostrar en algún log.
$texto = isset($data['texto']) ? htmlspecialchars(trim($data['texto']), ENT_QUOTES, 'UTF-8') : '';
$voz = isset($data['voz']) ? $data['voz'] : 'es-US-Standard-A';
$idioma = isset($data['idioma']) ? $data['idioma'] : 'es-US';

if (empty($texto)) {
    http_response_code(400); // Solicitud incorrecta
    echo json_encode(['error' => 'El campo de texto no puede estar vacío.']);
    exit;
}
if (mb_strlen($texto) > 5000) { // Límite de la API por solicitud
    http_response_code(413); // Payload Too Large
    echo json_encode(['error' => 'El texto excede el límite de 5000 caracteres.']);
    exit;
}


// --- SECCIÓN 3: CONSTRUCCIÓN DE LA SOLICITUD A LA API ---

$apiUrl = 'https://texttospeech.googleapis.com/v1/text:synthesize?key=' . GOOGLE_TTS_API_KEY;

$request_body = [
    'input' => [
        'text' => $texto // Usamos el texto sanitizado
    ],
    'voice' => [
        'languageCode' => $idioma,
        'name' => $voz
    ],
    'audioConfig' => [
        'audioEncoding' => 'MP3' // Formato de audio común y compatible
    ]
];

$payload = json_encode($request_body);

// --- SECCIÓN 4: EJECUCIÓN DE LA LLAMADA cURL ---

$ch = curl_init($apiUrl);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); // Siempre verificar SSL en producción
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); // Timeout de conexión
curl_setopt($ch, CURLOPT_TIMEOUT, 30); // Timeout total de la operación

$api_response_raw = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curl_error = curl_error($ch);
curl_close($ch);


// --- SECCIÓN 5: PROCESAMIENTO DE LA RESPUESTA ---

if ($curl_error) {
    http_response_code(500);
    echo json_encode(['error' => 'Error en cURL: ' . $curl_error]);
    exit;
}

if ($http_code == 200) {
    $api_response = json_decode($api_response_raw, true);
    if (isset($api_response['audioContent'])) {
        // La API devuelve el audio codificado en Base64.
        echo json_encode(['audioContent' => $api_response['audioContent']]);
    } else {
        http_response_code(500);
        echo json_encode(['error' => 'La API no devolvió contenido de audio.', 'details' => $api_response]);
    }
} else {
    http_response_code($http_code);
    $error_details = json_decode($api_response_raw, true);
    echo json_encode([
        'error' => 'Error al comunicarse con la API de Google.',
        'status_code' => $http_code,
        'details' => $error_details
    ]);
}
?>

Explicación del Código PHP:

  • Sección 1 y 2: Se define la API Key (debe ser externalizada), se valida el método HTTP y se reciben y sanitizan los datos de entrada. Se añade una validación para el límite de 5000 caracteres de la API.
  • Sección 3: Construye el cuerpo (`payload`) de la solicitud a la API, especificando el texto, la voz y el formato de audio.
  • Sección 4: Utiliza cURL para hacer la solicitud POST a la API de Google. Se han añadido timeouts para evitar que el script se cuelgue indefinidamente.
  • Sección 5: Analiza la respuesta. Si es exitosa (código 200), extrae el audio en Base64 y lo envía al frontend. Si no, devuelve un error detallado para facilitar la depuración.

🖥️ Paso 3: La Interfaz de Usuario (Frontend con HTML, JS y Bootstrap)

Este será el archivo que los usuarios verán. Contiene un formulario para escribir el texto y un reproductor de audio para escuchar el resultado.

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Texto a Voz con PHP y Google API</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <!-- Directriz #7: jQuery -->
    <script src="https://code.jquery.com/jquery-3.7.1.js"></script>
</head>
<body>
    <div class="container mt-5">
        <div class="card">
            <div class="card-header">
                <h3>Conversor de Texto a Voz</h3>
            </div>
            <div class="card-body">
                <form id="tts-form">
                    <div class="mb-3">
                        <label for="texto" class="form-label">Escribe el texto a convertir (máx. 5000 caracteres):</label>
                        <textarea class="form-control" id="texto" rows="4" required maxlength="5000">Hola mundo, estoy usando la API de Google para hablar.</textarea>
                        <div id="char-count" class="form-text">0 / 5000</div>
                    </div>
                    <div class="mb-3">
                        <label for="voz" class="form-label">Selecciona una voz:</label>
                        <select class="form-select" id="voz">
                            <option value="es-US-Standard-A" data-lang="es-US">Español (EE.UU.) - Femenina A</option>
                            <option value="es-US-Standard-B" data-lang="es-US">Español (EE.UU.) - Masculina B</option>
                            <option value="es-ES-Standard-A" data-lang="es-ES">Español (España) - Femenina A</option>
                            <option value="en-US-Journey-F" data-lang="en-US">Inglés (EE.UU.) - Journey F (Premium)</option>
                            <option value="fr-FR-Standard-E" data-lang="fr-FR">Francés (Francia) - Femenina E</option>
                        </select>
                    </div>
                    <button type="submit" id="btn-generar" class="btn btn-primary">
                        <span id="btn-text">Generar y Escuchar Audio</span>
                        <span id="btn-spinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
                    </button>
                </form>
            </div>
            <div class="card-footer" id="resultado-audio" style="display: none;">
                <h5>Resultado:</h5>
                <audio controls id="audio-player" class="w-100"></audio>
                <div id="error-container" class="alert alert-danger mt-3 d-none"></div>
            </div>
        </div>
    </div>

<script>
// Directriz #6: Estructura de Scripts JavaScript Modulares
document.addEventListener('DOMContentLoaded', function() {

    //================================
    // NOMBRE DEL BLOQUE: INICIALIZACIÓN Y VARIABLES GLOBALES
    // Explicación: Selecciona elementos del DOM y establece listeners iniciales.
    //================================
    const ttsForm = document.getElementById('tts-form');
    const textoInput = document.getElementById('texto');
    const charCount = document.getElementById('char-count');
    const MAX_CHARS = 5000;
    
    // Inicializar contador
    charCount.textContent = `${textoInput.value.length} / ${MAX_CHARS}`;

    textoInput.addEventListener('input', () => {
        charCount.textContent = `${textoInput.value.length} / ${MAX_CHARS}`;
    });
    //================================ FIN CÓDIGO [INICIALIZACIÓN Y VARIABLES GLOBALES]

    //================================
    // NOMBRE DEL BLOQUE: Manejo del Formulario TTS.
    // Explicación: Gestiona el envío del formulario. Previene el comportamiento por defecto,
    // recolecta los datos y llama a la función que se comunica con el backend.
    //================================
    ttsForm.addEventListener('submit', function(event) {
        event.preventDefault();

        const texto = textoInput.value;
        const vozSelect = document.getElementById('voz');
        const selectedOption = vozSelect.options[vozSelect.selectedIndex];
        const voz = selectedOption.value;
        const idioma = selectedOption.dataset.lang;

        toggleButtonState(true);
        document.getElementById('error-container').classList.add('d-none');

        llamarApiTextoAVoz(texto, voz, idioma);
    });
    //================================ FIN CÓDIGO [Manejo del Formulario TTS]

    //================================
    // NOMBRE DEL BLOQUE: Comunicación con API Backend.
    // Explicación: Envía los datos al script PHP 'generar_audio.php' usando fetch.
    // Si la respuesta es exitosa, procesa el audio; si no, muestra el error.
    //================================
    async function llamarApiTextoAVoz(texto, voz, idioma) {
        try {
            const response = await fetch('generar_audio.php', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ texto, voz, idioma })
            });

            const data = await response.json();

            if (!response.ok) {
                let errorMsg = `Error ${response.status}: `;
                if (data && data.error) {
                    errorMsg += data.error;
                    if(data.details && data.details.error && data.details.error.message) {
                        errorMsg += ` - Detalles: ${data.details.error.message}`;
                    }
                } else {
                    errorMsg += 'Respuesta inesperada del servidor.';
                }
                throw new Error(errorMsg);
            }

            if (data.audioContent) {
                const audioPlayer = document.getElementById('audio-player');
                audioPlayer.src = `data:audio/mp3;base64,${data.audioContent}`;
                audioPlayer.play();
                document.getElementById('resultado-audio').style.display = 'block';
            } else {
                throw new Error(data.error || 'La respuesta no contiene audio.');
            }
        } catch (error) {
            console.error('Error en la solicitud fetch:', error);
            mostrarError(error.message);
        } finally {
            toggleButtonState(false);
        }
    }
    //================================ FIN CÓDIGO [Comunicación con API Backend]
    
    //================================
    // NOMBRE DEL BLOQUE: Funciones de UI Auxiliares.
    // Explicación: Contiene funciones para manipular la interfaz, como mostrar errores
    // y cambiar el estado del botón de envío (habilitado/deshabilitado con spinner).
    //================================
    function toggleButtonState(isLoading) {
        const btn = document.getElementById('btn-generar');
        const text = document.getElementById('btn-text');
        const spinner = document.getElementById('btn-spinner');

        if (isLoading) {
            btn.disabled = true;
            text.style.display = 'none';
            spinner.classList.remove('d-none');
        } else {
            btn.disabled = false;
            text.style.display = 'inline';
            spinner.classList.add('d-none');
        }
    }

    function mostrarError(mensaje) {
        const errorContainer = document.getElementById('error-container');
        errorContainer.textContent = mensaje;
        errorContainer.classList.remove('d-none');
        document.getElementById('resultado-audio').style.display = 'block';
    }
    //================================ FIN CÓDIGO [Funciones de UI Auxiliares]

});
</script>
</body>
</html>

Explicación del Código JS:

  • Inicialización: Se añade un contador de caracteres para guiar al usuario.
  • Manejo del Formulario: Se usa JavaScript nativo (en lugar de jQuery) para seguir las buenas prácticas modernas. Intercepta el evento `submit`, previene la recarga y llama a la función de comunicación.
  • Comunicación Asíncrona: La función `llamarApiTextoAVoz` se define como `async` y utiliza `await` para un manejo más limpio del código asíncrono. El bloque `try...catch...finally` asegura que la interfaz se actualice correctamente, incluso si ocurren errores.
  • Procesamiento de la Respuesta: Si la solicitud es exitosa, construye un **Data URL** a partir del Base64 recibido y lo asigna al `src` del elemento `
  • Manejo Detallado de Errores: Captura y muestra los mensajes de error específicos devueltos por el backend PHP, lo que facilita enormemente la depuración.

💾 Paso 4 (Opcional): Persistencia en Base de Datos MySQL

Guardar un registro de las conversiones es útil para cacheo, auditoría o análisis. Aquí te muestro cómo estructurar la tabla y la lógica de inserción.

Estructura de la Tabla SQL

Puedes usar esta consulta para crear la tabla `audios_generados`.

CREATE TABLE `audios_generados` (
  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
  `hash_texto` CHAR(64) NOT NULL COMMENT 'SHA256 del texto y la voz para búsqueda rápida',
  `texto_origen` TEXT NOT NULL,
  `voz_usada` VARCHAR(50) NOT NULL,
  `idioma_usado` VARCHAR(10) NOT NULL,
  `audio_base64` LONGTEXT CHARACTER SET 'ascii' COLLATE 'ascii_general_ci' NOT NULL COMMENT 'Datos del audio en Base64',
  `fecha_creacion` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `ultimo_acceso` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_hash_texto` (`hash_texto`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Lógica de Implementación (Cacheo)

Antes de llamar a la API de Google, puedes comprobar si ya generaste ese audio.

  1. En `generar_audio.php`, antes de la llamada cURL:
  2. Calcula un hash único para la combinación de texto y voz: `$hash = hash('sha256', $texto . $voz);`
  3. Prepara y ejecuta una consulta a la BD: `SELECT audio_base64 FROM audios_generados WHERE hash_texto = ?`
  4. Si encuentras un resultado:
    • Devuelve el `audio_base64` directamente desde la base de datos.
    • Actualiza el campo `ultimo_acceso`.
    • Esto te ahorra una llamada a la API (y su costo).
  5. Si no encuentras un resultado:
    • Procede con la llamada cURL a la API de Google.
    • Tras recibir la respuesta exitosa, inserta un nuevo registro en `audios_generados` con el hash, texto, voz y el `audioContent` recibido.

🚀 Buenas Prácticas, Escalabilidad y Errores Comunes

Seguridad

  • Gestión de API Keys: Como se mencionó, NUNCA dejes la clave en el código. Usa variables de entorno (`getenv('GOOGLE_API_KEY')`) o archivos de configuración (`.env`) cargados fuera del directorio web público.
  • Validación de Entrada: Sanitiza y valida siempre los datos del usuario. Limita la longitud del texto para prevenir abusos y payloads excesivos.
  • Restricción de IP (si es posible): Si tu aplicación solo será llamada desde tu propio servidor, puedes restringir el uso de la clave de API a la dirección IP de tu servidor en la consola de Google Cloud para una capa extra de seguridad.

Rendimiento y Escalabilidad

  • Cacheo: La estrategia de cacheo con base de datos descrita en el Paso 4 es fundamental. Evita generar el mismo audio repetidamente, ahorrando costos y tiempo de respuesta. Para sitios de alto tráfico, considera un sistema de caché más rápido como Redis o Memcached.
  • Tareas en Segundo Plano (Colas): Si permites la conversión de textos muy largos (por ejemplo, capítulos de un libro), la solicitud puede tardar demasiado y causar un `timeout`. En estos casos, la mejor solución es usar un sistema de colas (como Beanstalkd o RabbitMQ con un worker PHP). El flujo sería:
    1. El usuario envía el texto.
    2. PHP añade una "tarea" a la cola y responde inmediatamente al usuario: "Tu audio se está procesando y te notificaremos cuando esté listo".
    3. Un script "worker" (un proceso PHP corriendo constantemente en el servidor) toma la tarea de la cola, llama a la API de Google, y guarda el resultado en la BD o en un archivo.

Extensibilidad: Patrón Adaptador

Imagina que en el futuro quieres cambiar a la API de texto a voz de AWS o Azure. Si tu código está acoplado a Google, tendrías que reescribir mucho. Usando el patrón de diseño "Adaptador", puedes evitarlo:

// Interfaz común
interface TextToSpeechInterface {
    public function synthesize(string $text, string $voice): string; // Devuelve audio en Base64
}

// Adaptador para Google
class GoogleTTSAdapter implements TextToSpeechInterface {
    public function synthesize(string $text, string $voice): string {
        // Aquí va toda la lógica de cURL para Google...
        return $audioContent;
    }
}

// Adaptador para otro servicio
class AwsPollyAdapter implements TextToSpeechInterface {
    public function synthesize(string $text, string $voice): string {
        // Aquí iría la lógica del SDK o cURL para AWS Polly...
        return $audioContent;
    }
}

// En tu controlador
// $ttsService = new GoogleTTSAdapter();
$ttsService = new AwsPollyAdapter(); // Cambiar de proveedor es así de fácil
$audio = $ttsService->synthesize('Hola mundo', 'es-US-Standard-A');

Errores Comunes y Soluciones

  • Error 403 - Permission Denied:
    • Causa: La clave de API es incorrecta, está mal restringida (p.ej. por IP) o no has habilitado la API en la consola de Google.
    • Solución: Verifica la clave, sus restricciones y que la "Cloud Text-to-Speech API" esté habilitada para tu proyecto.
  • Error 429 - Quota Exceeded:
    • Causa: Has superado el límite de solicitudes por minuto o el límite mensual del nivel gratuito.
    • Solución: Implementa cacheo para reducir llamadas. Si el uso es legítimo, considera aumentar las cuotas en la consola de Google Cloud (puede implicar costos).
  • Error 400 - Invalid Argument:
    • Causa: El cuerpo de la solicitud (payload) es incorrecto. Puede ser una voz que no existe (`es-XX-MiVoz`), un código de idioma no válido o texto vacío.
    • Solución: Revisa el JSON que estás enviando. Asegúrate de que los nombres de las voces y los códigos de idioma sean los correctos según la documentación oficial.
  • Error de cURL (SSL certificate problem):
    • Causa: Tu servidor PHP no tiene un paquete de certificados CA actualizado para verificar el certificado SSL de Google.
    • Solución: NO desactives la verificación SSL (`CURLOPT_SSL_VERIFYPEER` a `false`) en producción. La solución correcta es actualizar el paquete de `ca-certificates` en tu servidor o especificar la ruta a un archivo `cacert.pem` actualizado usando `curl_setopt($ch, CURLOPT_CAINFO, '/ruta/a/cacert.pem');`.