Tutorial 17: Guardando la Transcripción en la Base de Datos
Tutorial 17: Guardando la Transcripción en la Base de Datos
¡Hola y bienvenido de nuevo!
En esta lección vamos a crear un método para guardar la información del archivo y su transcripción en la base de datos. Esto nos permitirá mostrar los archivos generados recientemente y acceder a ellos en cualquier momento. ¡Vamos a ello!
📋 Contenido del Tutorial
Preparando la base de datos
Creando el método save
Usando Prepared Statements
Determinando el tipo de archivo
Redirigiendo al usuario
Código completo
Explicación detallada
Próximos pasos
🗄️ 1. Preparando la base de datos
Estructura de la tabla files
Recordemos la estructura de nuestra tabla:
CREATE TABLE `files` ( `ID` int(11) NOT NULL AUTO_INCREMENT, `content` text DEFAULT NULL, `fileUrl` varchar(255) NOT NULL, `translated` text DEFAULT NULL, `lang` varchar(255) DEFAULT NULL, `type` enum('audio','video') NOT NULL, PRIMARY KEY (`ID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Columnas que vamos a usar
| Columna | Descripción | Valor que guardaremos |
|---|---|---|
fileUrl | Ruta del archivo | files/abc123.mp4 |
content | Transcripción | Texto de la transcripción |
type | Tipo de archivo | audio o video |
¿Por qué no guardamos todo?
| Columna | ¿Por qué no? | Explicación |
|---|---|---|
translated | Lo guardaremos después | Se llena cuando el usuario traduce |
lang | Lo guardaremos después | Se llena cuando el usuario traduce |
ID | Auto-increment | MySQL lo genera automáticamente |
💾 2. Creando el método save
Estructura del método
public function save($file, $content, $type) { // 1. Preparar la consulta SQL $stmt = $this->DB->prepare( "INSERT INTO `files` (`fileUrl`, `content`, `type`) VALUES (:fileUrl, :content, :type)" ); // 2. Vincular los parámetros $stmt->bindParam(":fileUrl", $file, PDO::PARAM_STR); $stmt->bindParam(":content", $content, PDO::PARAM_STR); $stmt->bindParam(":type", $type, PDO::PARAM_STR); // 3. Ejecutar la consulta $stmt->execute(); // 4. Retornar el ID del registro insertado return $this->DB->lastInsertId(); }
¿Qué hace cada parte?
| Parte | Función | Explicación |
|---|---|---|
prepare() | Prepara la consulta | Evita inyección SQL |
bindParam() | Vincula parámetros | Asigna valores a los placeholders |
execute() | Ejecuta la consulta | Inserta los datos en la BD |
lastInsertId() | Obtiene el ID | Retorna el ID del nuevo registro |
🔒 3. Usando Prepared Statements
¿Qué son los Prepared Statements?
Los Prepared Statements (Sentencias Preparadas) son una característica de PDO que permite ejecutar consultas SQL de forma segura, separando la estructura SQL de los datos.
¿Por qué usarlos?
| Razón | Explicación |
|---|---|
| Seguridad | Previene inyección SQL |
| Rendimiento | Las consultas se compilan una sola vez |
| Legibilidad | El código es más limpio |
Ejemplo de inyección SQL (sin preparar)
// ❌ PELIGROSO - No usar $sql = "INSERT INTO files (fileUrl) VALUES ('$file')"; // Si $file = "archivo.mp4'); DROP TABLE files; --" // La tabla sería eliminada!
Ejemplo con Prepared Statements (seguro)
// ✅ SEGURO $stmt = $this->DB->prepare( "INSERT INTO files (fileUrl) VALUES (:fileUrl)" ); $stmt->bindParam(":fileUrl", $file, PDO::PARAM_STR); $stmt->execute(); // Los datos se tratan como datos, no como SQL
Placeholders en PDO
| Tipo | Sintaxis | Ejemplo |
|---|---|---|
| Nombre | :nombre | :fileUrl |
| Posicional | ? | INSERT INTO files VALUES (?, ?) |
Recomendación: Usar placeholders con nombre para mejor legibilidad.
🏷️ 4. Determinando el tipo de archivo
¿Cómo sabemos si es audio o video?
Usamos el tipo MIME del archivo:
// Obtener el tipo MIME $fileType = $_FILES['file']['type']; // Verificar si es audio if (strpos($fileType, 'audio/') === 0) { $type = 'audio'; } // Verificar si es video else if (strpos($fileType, 'video/') === 0) { $type = 'video'; }
¿Qué hace strpos()?
strpos() busca una subcadena dentro de otra:
// Ejemplo con audio $fileType = 'audio/mpeg'; strpos($fileType, 'audio/') === 0; // true (0) $fileType = 'video/mp4'; strpos($fileType, 'audio/') === 0; // false (-1)
Tipos MIME comunes
| Archivo | Tipo MIME | Categoría |
|---|---|---|
| MP3 | audio/mpeg | audio |
| WAV | audio/wav | audio |
| M4A | audio/m4a | audio |
| MP4 | video/mp4 | video |
| AVI | video/x-msvideo | video |
| MOV | video/quicktime | video |
🔄 5. Redirigiendo al usuario
¿Por qué redirigir?
Después de procesar el archivo, queremos mostrar la transcripción al usuario. La redirección nos permite:
Separar responsabilidades: El index.php se encarga de la subida, view.php de la visualización
URL limpia: El usuario ve la URL del archivo específico
Recarga segura: Si el usuario recarga, no se reprocesa el archivo
Código de redirección
$fileID = $whisperObj->save($file, $text['text'], $type); header("location: view.php?file={$fileID}"); exit; // Importante: detener la ejecución
Estructura de la URL
http://localhost/whisper/view.php?file=1
↑ ↑
| └─ ID del archivo (1)
└─ Parámetro en la URL¿Qué sigue?
En view.php, usaremos el ID para recuperar los datos:
// view.php (por crear) $id = $_GET['file'] ?? 0; $fileData = $whisperObj->getFile($id); // Mostrar la transcripción
💻 6. Código completo
Archivo: backend/classes/Whisper.php (método save agregado)
<?php class Whisper{ public $error; public $dataType; public $file; public $lang; public $content; private $DB; public function __construct(){ $db = new DB; $this->DB = $db->connect(); } public function errors(){ return $this->error; } public function getApiUrl(){ if($this->dataType === "ASR"){ return "https://api.openai.com/v1/audio/transcriptions"; }else{ return "https://api.openai.com/v1/chat/completions"; } } public function getHeader(){ if($this->dataType === "ASR"){ return [ 'Authorization: Bearer ' . API_TOKEN, 'Content-Type: multipart/form-data' ]; }else{ return [ 'Authorization: Bearer ' . API_TOKEN, 'Content-Type: application/json' ]; } } public function getData(){ if($this->dataType === "ASR"){ return [ 'file' => $this->file, 'model' => 'whisper-1', 'language' => 'es', 'response_format' => 'json' ]; }else{ return json_encode([ 'model' => 'gpt-3.5-turbo', 'messages' => [ [ 'role' => 'system', 'content' => 'You will be provided with a text, and your task is to translate it into ' . $this->lang ], [ 'role' => 'user', 'content' => $this->content ] ] ]); } } public function getFile(){ if($this->dataType === 'ASR'){ if (file_exists($this->file)) { $this->file = curl_file_create($this->file); } else { $this->error = "El archivo no existe: " . $this->file; return false; } } return true; } public function convert(){ $apiUrl = $this->getApiUrl(); $ch = curl_init($apiUrl); if (!$ch) { $this->error = "Error al inicializar cURL"; return false; } curl_setopt($ch, CURLOPT_POST, true); if (!$this->getFile()) { return false; } curl_setopt($ch, CURLOPT_POSTFIELDS, $this->getData()); curl_setopt($ch, CURLOPT_HTTPHEADER, $this->getHeader()); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 120); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $curlError = curl_error($ch); curl_close($ch); if ($curlError) { $this->error = "Error cURL: " . $curlError; return false; } if ($httpCode !== 200) { $this->error = "Error HTTP: " . $httpCode; return false; } if($response){ $data = json_decode($response, true); if (isset($data['error'])) { $this->error = "Error API: " . $data['error']['message']; return false; } return $data; }else{ $this->error = "API REQUEST FAILED"; return false; } } public function upload($file){ $fileTmp = $file['tmp_name']; $filename = basename($file['name']); $fileSize = $file['size']; $errors = $file['error']; $mime = $file['type']; //get file extension $ext = pathinfo($filename, PATHINFO_EXTENSION); $ext = strtolower($ext); $parentDirectoy = dirname(dirname(dirname(__FILE__))); $allowedMedia = ['video/mp4','video/mpeg', 'audio/mpeg','audio/mpeg3','audio/wav']; if(in_array($mime, $allowedMedia)){ if($fileSize <= 20000000){ $folder = 'files/'; $file = $folder. md5(time() . mt_rand()) . '.'.$ext; move_uploaded_file($fileTmp, $parentDirectoy . '/'.$file); return $file; }else{ $this->error = "El archivo es demasiado grande (máximo 20 MB)"; return false; } }else{ $this->error = "Formato de archivo inválido"; return false; } } /** * Guarda la información del archivo en la base de datos * * @param string $file Ruta del archivo * @param string $content Contenido transcrito * @param string $type Tipo de archivo (audio/video) * @return int|false ID del registro insertado o false en caso de error */ public function save($file, $content, $type){ try { // Preparar la consulta SQL con placeholders $stmt = $this->DB->prepare( "INSERT INTO `files` (`fileUrl`, `content`, `type`) VALUES (:fileUrl, :content, :type)" ); // Vincular los parámetros para evitar inyección SQL $stmt->bindParam(":fileUrl", $file, PDO::PARAM_STR); $stmt->bindParam(":content", $content, PDO::PARAM_STR); $stmt->bindParam(":type", $type, PDO::PARAM_STR); // Ejecutar la consulta $stmt->execute(); // Retornar el ID del registro insertado return $this->DB->lastInsertId(); } catch (PDOException $e) { $this->error = "Error al guardar en la base de datos: " . $e->getMessage(); return false; } } } ?>
Archivo: index.php (versión completa)
<?php include 'backend/init.php'; $error = null; if ($_SERVER['REQUEST_METHOD'] === "POST") { if (isset($_FILES['file']) && !empty($_FILES['file']['name'])) { // 1. Obtener el tipo MIME del archivo $fileType = $_FILES['file']['type']; // 2. Subir el archivo $file = $whisperObj->upload($_FILES['file']); if ($file) { // 3. Configurar para transcripción $whisperObj->dataType = 'ASR'; $whisperObj->file = $file; // 4. Transcribir el archivo $text = $whisperObj->convert(); if ($text) { // 5. Determinar el tipo de archivo (audio o video) if (strpos($fileType, 'audio/') === 0) { $type = 'audio'; } else if (strpos($fileType, 'video/') === 0) { $type = 'video'; } else { // Por defecto, si no se reconoce $type = 'video'; } // 6. Guardar en la base de datos $fileID = $whisperObj->save($file, $text['text'], $type); if ($fileID) { // 7. Redirigir a la página de visualización header("location: view.php?file={$fileID}"); exit; } else { $error = $whisperObj->errors(); } } else { $error = $whisperObj->errors(); } } else { $error = $whisperObj->errors(); } } else { $error = "Por favor selecciona un archivo para convertir a texto"; } } ?> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title> My Whisper Ai - Convert Video/Audio language into written text </title> <!-- FONT-AWESOME LINK --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" integrity="sha512-z3gLpd7yknf1YoNbCzqRKc4qyor8gaKU1qmn+CShxbuBusANI9QpRohGBreCFkKxLhei6S9CQXFEbbKuqLg0DA==" crossorigin="anonymous" referrerpolicy="no-referrer" /> <!-- CSS LINK --> <link rel="stylesheet" type="text/css" href="frontend/style/style.css"/> <!-- TAILWIND CSS LINK --> <script src="https://cdn.tailwindcss.com"></script> <!-- GOOGLE FONTS LINK --> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Varela+Round&display=swap" rel="stylesheet"> <!-- JQUERY LINK --> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> </head> <body class=""> <div class="Wrapper"> <div class="inner-wrapper h-screen flex justify-center p-20"> <div class="h-full border-2 rounded-xl border-gray-700" style="width: 1280px; max-height: 1080px; box-shadow: -20px 20px 0px 0px #74a840,-21px 21px 0px 3px #343333"> <div class="flex h-full flex-col items-center"> <!--loader--> <div id="loader" class="hidden flex flex-col text-center flex-1 " style="width: 600px;"> <div class="flex items-center flex-col"> <img class="w-40 animate-pulse mt-60" src="frontend/images/loader.gif"/> <span class="animate-pulse"> Uploading.... </span> </div> </div> <div id="upload" class=" flex flex-col text-center mt-40" style="width: 600px;"> <img class="mx-auto my-4" src="frontend/images/banner-img.png" style="width: 300px;"> <h1 class="text-4xl py-2">Welcome to My Whisper</h1> <p class="text-xl"> Whisper is an AI tool that allows you to convert video/audio files into text and also translate the text into your desired language. </p> <label style="width: 200px;box-shadow: -7px 6px 0px 0px; border: 1px solid;" class="cursor-pointer select-none mx-auto my-4 rounded-full text-xl text-gray-600 px-10 py-5 border bg-lime-400" for="file-upload"> Upload File </label> <form id="form" method="post" enctype="multipart/form-data"> <input id="file-upload" class="hidden "type="file" name="file"> </form> <!-- ERROR DIV --> <?php if(isset($error)): ?> <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative"> <strong class="font-bold">Error:</strong> <span class="block sm:inline"><?php echo $error; ?></span> </div> <?php endif; ?> <p>Recently Generated Files : Click <a href="view.php?file=1" class="text-green font-bold text-sm">Here to View</a></p> </div> </div> </div> </div> </div> <!-- JS CODE --> <script> $(document).ready(function(){ $("#file-upload").change(function(){ if(this.files.length > 0){ $("#loader").removeClass('hidden'); $("#upload").addClass('hidden'); $("#form").submit(); } }); }); </script> </body> </html>
📖 7. Explicación detallada
El método save paso a paso
public function save($file, $content, $type) { try { // 1. Preparar la consulta $stmt = $this->DB->prepare( "INSERT INTO `files` (`fileUrl`, `content`, `type`) VALUES (:fileUrl, :content, :type)" ); // 2. Vincular parámetros $stmt->bindParam(":fileUrl", $file, PDO::PARAM_STR); $stmt->bindParam(":content", $content, PDO::PARAM_STR); $stmt->bindParam(":type", $type, PDO::PARAM_STR); // 3. Ejecutar $stmt->execute(); // 4. Obtener ID return $this->DB->lastInsertId(); } catch (PDOException $e) { $this->error = "Error al guardar: " . $e->getMessage(); return false; } }
¿Qué es PDO::PARAM_STR?
PDO::PARAM_STR es una constante que le dice a PDO que el parámetro es una cadena de texto.
$stmt->bindParam(":fileUrl", $file, PDO::PARAM_STR);
Otros tipos de parámetros:
| Constante | Tipo | Uso |
|---|---|---|
PDO::PARAM_STR | String | Texto |
PDO::PARAM_INT | Integer | Números enteros |
PDO::PARAM_BOOL | Boolean | Verdadero/Falso |
PDO::PARAM_NULL | NULL | Valor nulo |
¿Qué es lastInsertId()?
lastInsertId() devuelve el ID de la última fila insertada:
// Después de un INSERT $id = $this->DB->lastInsertId(); // $id = 1 (si es el primer registro) // $id = 2 (si es el segundo) // etc.
El flujo completo en index.php
1. Usuario sube archivo ↓ 2. upload() guarda en files/ ↓ 3. dataType = 'ASR', file = $file ↓ 4. convert() envía a la API ↓ 5. Se recibe la transcripción ↓ 6. Se determina el tipo (audio/video) ↓ 7. save() guarda en la base de datos ↓ 8. Se obtiene el ID del registro ↓ 9. Redirección a view.php?file=ID
🧪 8. Probando la aplicación
Paso 1: Verificar la base de datos
-- Verificar que la tabla existe DESCRIBE files; -- Verificar que está vacía SELECT * FROM files;
Paso 2: Subir un archivo
Ve a
http://localhost/whisper/Selecciona un archivo de audio o video
Espera el procesamiento
Paso 3: Verificar el guardado
-- Ver los registros insertados SELECT * FROM files ORDER BY ID DESC; -- Ver la transcripción SELECT ID, fileUrl, LEFT(content, 100) as preview, type FROM files ORDER BY ID DESC;
Paso 4: Verificar la redirección
Después de procesar, deberías ser redirigido a:
http://localhost/whisper/view.php?file=1
(Aunque view.php aún no existe, la URL es correcta)
📊 9. Resumen del flujo completo
Diagrama de secuencia
Usuario → index.php → Whisper → API → Base de Datos → view.php
↓ ↓ ↓ ↓ ↓ ↓
Sube Recibe Procesa Transcribe Guarda Muestra
archivo archivo archivo audio datos resultadoTabla de datos guardados
| Campo | Fuente | Ejemplo |
|---|---|---|
fileUrl | $file | files/abc123.mp4 |
content | $text['text'] | "Hello and welcome..." |
type | $type | audio o video |
❓ Preguntas frecuentes
¿Qué pasa si falla el guardado en la base de datos?
El método
save()retornafalseSe captura la excepción y se guarda el error
El usuario ve el mensaje de error
¿Por qué usar :fileUrl y no ??
Los placeholders con nombre son más legibles
Facilitan el mantenimiento del código
¿Qué es strpos() y por qué === 0?
strpos()busca una subcadena=== 0verifica que esté al inicio del string
¿Qué pasa si el tipo no es ni audio ni video?
Se usa
videocomo valor por defectoEs mejor validar más exhaustivamente
¿Por qué redirigir después de guardar?
Para evitar reenvíos del formulario
Para mostrar la transcripción en su propia página
🎯 Próximos pasos
Lo que hemos logrado
✅ Método save() implementado
✅ Guardado en base de datos con PDO
✅ Determinación del tipo de archivo
✅ Redirección a la página de vista
Lo que viene en la próxima lección
En la siguiente lección vamos a:
Crear la página
view.phppara mostrar transcripcionesRecuperar datos de la base de datos
Mostrar el reproductor de audio/video
Implementar la traducción del texto
Avance del código de la próxima lección
// view.php <?php include 'backend/init.php'; $id = $_GET['file'] ?? 0; $file = $whisperObj->getFileById($id); // Mostrar la transcripción echo $file['content']; // Mostrar el reproductor echo '<audio controls><source src="' . $file['fileUrl'] . '"></audio>'; ?>
¡Excelente trabajo! Ahora tenemos una aplicación que sube archivos, los transcribe y guarda todo en la base de datos. En la próxima lección, crearemos la página para visualizar las transcripciones.
Comentarios
Publicar un comentario