Integración completa con PHP, cURL, Bootstrap y MySQL para aplicaciones web dinámicas y accesibles.
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:
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.
Para usar la API, necesitas un proyecto de Google Cloud y una clave de API.
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`).
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
]);
}
?>
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>
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.
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;
Antes de llamar a la API de Google, puedes comprobar si ya generaste ese audio.
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');