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

  1. Preparando la base de datos

  2. Creando el método save

  3. Usando Prepared Statements

  4. Determinando el tipo de archivo

  5. Redirigiendo al usuario

  6. Código completo

  7. Explicación detallada

  8. Próximos pasos


🗄️ 1. Preparando la base de datos

Estructura de la tabla files

Recordemos la estructura de nuestra tabla:

sql
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

ColumnaDescripciónValor que guardaremos
fileUrlRuta del archivofiles/abc123.mp4
contentTranscripciónTexto de la transcripción
typeTipo de archivoaudio o video

¿Por qué no guardamos todo?

Columna¿Por qué no?Explicación
translatedLo guardaremos despuésSe llena cuando el usuario traduce
langLo guardaremos despuésSe llena cuando el usuario traduce
IDAuto-incrementMySQL lo genera automáticamente

💾 2. Creando el método save

Estructura del método

php
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?

ParteFunciónExplicación
prepare()Prepara la consultaEvita inyección SQL
bindParam()Vincula parámetrosAsigna valores a los placeholders
execute()Ejecuta la consultaInserta los datos en la BD
lastInsertId()Obtiene el IDRetorna 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ónExplicación
SeguridadPreviene inyección SQL
RendimientoLas consultas se compilan una sola vez
LegibilidadEl código es más limpio

Ejemplo de inyección SQL (sin preparar)

php
// ❌ 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)

php
// ✅ 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

TipoSintaxisEjemplo
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:

php
// 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:

php
// Ejemplo con audio
$fileType = 'audio/mpeg';
strpos($fileType, 'audio/') === 0; // true (0)

$fileType = 'video/mp4';
strpos($fileType, 'audio/') === 0; // false (-1)

Tipos MIME comunes

ArchivoTipo MIMECategoría
MP3audio/mpegaudio
WAVaudio/wavaudio
M4Aaudio/m4aaudio
MP4video/mp4video
AVIvideo/x-msvideovideo
MOVvideo/quicktimevideo

🔄 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:

  1. Separar responsabilidades: El index.php se encarga de la subida, view.php de la visualización

  2. URL limpia: El usuario ve la URL del archivo específico

  3. Recarga segura: Si el usuario recarga, no se reprocesa el archivo

Código de redirección

php
$fileID = $whisperObj->save($file, $text['text'], $type);
header("location: view.php?file={$fileID}");
exit; // Importante: detener la ejecución

Estructura de la URL

text
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:

php
// 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
<?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
<?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

php
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.

php
$stmt->bindParam(":fileUrl", $file, PDO::PARAM_STR);

Otros tipos de parámetros:

ConstanteTipoUso
PDO::PARAM_STRStringTexto
PDO::PARAM_INTIntegerNúmeros enteros
PDO::PARAM_BOOLBooleanVerdadero/Falso
PDO::PARAM_NULLNULLValor nulo

¿Qué es lastInsertId()?

lastInsertId() devuelve el ID de la última fila insertada:

php
// 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

text
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

sql
-- Verificar que la tabla existe
DESCRIBE files;

-- Verificar que está vacía
SELECT * FROM files;

Paso 2: Subir un archivo

  1. Ve a http://localhost/whisper/

  2. Selecciona un archivo de audio o video

  3. Espera el procesamiento

Paso 3: Verificar el guardado

sql
-- 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:

text
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

text
Usuario → index.php → Whisper → API → Base de Datos → view.php
    ↓          ↓          ↓        ↓          ↓            ↓
 Sube     Recibe    Procesa   Transcribe  Guarda     Muestra
 archivo  archivo    archivo    audio     datos     resultado

Tabla de datos guardados

CampoFuenteEjemplo
fileUrl$filefiles/abc123.mp4
content$text['text']"Hello and welcome..."
type$typeaudio o video

❓ Preguntas frecuentes

¿Qué pasa si falla el guardado en la base de datos?

  • El método save() retorna false

  • Se 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

  • === 0 verifica que esté al inicio del string

¿Qué pasa si el tipo no es ni audio ni video?

  • Se usa video como valor por defecto

  • Es 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:

  1. Crear la página view.php para mostrar transcripciones

  2. Recuperar datos de la base de datos

  3. Mostrar el reproductor de audio/video

  4. Implementar la traducción del texto

Avance del código de la próxima lección

php
// 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

Entradas más populares de este blog

Cómo usar Whisper para sacar el texto de un video

Tutorial 18: Creando la Página de Visualización de Archivos Recientes

Tutorial 11: ¿Qué es Whisper AI y Cómo Funciona?