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

 

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

¡Hola y bienvenido de nuevo!

En esta lección vamos a crear la página view.php donde mostraremos los archivos generados recientemente, su transcripción y un reproductor de audio/video. Además, implementaremos la funcionalidad para listar todos los archivos guardados en la base de datos. ¡Vamos a ello!


📋 Contenido del Tutorial

  1. Creando la página view.php

  2. El método getRecentList

  3. Mostrando la lista de archivos

  4. Usando el operador ternario

  5. Corrigiendo la base de datos

  6. Código completo

  7. Explicación detallada

  8. Próximos pasos


📄 1. Creando la página view.php

Estructura de la página

La página view.php tiene varias secciones importantes:

text
┌─────────────────────────────────────────────────────┐
│  My Whisper AI                         [Back]      │
├─────────────────────────────────────────────────────┤
│                                                     │
│  ┌──────────────┐  ┌──────────────────────────┐   │
│  │  Reproductor  │  │   Transcripción          │   │
│  │  (Audio/Vid)  │  │   Texto completo         │   │
│  │              │  │                          │   │
│  │              │  │                          │   │
│  ├──────────────┤  │   [Select Language]      │   │
│  │  Archivos    │  │   [Translate Button]     │   │
│  │  Recientes   │  │                          │   │
│  │  - archivo1  │  │                          │   │
│  │  - archivo2  │  │                          │   │
│  │  - archivo3  │  │                          │   │
│  └──────────────┘  └──────────────────────────┘   │
│                                                     │
└─────────────────────────────────────────────────────┘

Creando el archivo

  1. Crea un nuevo archivo en la raíz del proyecto

  2. Nómbralo view.php

  3. Copia el código de view.html (proporcionado en los archivos)

Estructura del HTML

php
<?php 
    include 'backend/init.php';
?>
<!DOCTYPE html>
<html>
<head>
    <!-- Headers y estilos -->
</head>
<body>
    <!-- Header con título y botón Back -->
    <!-- Reproductor de audio/video -->
    <!-- Lista de archivos recientes -->
    <!-- Transcripción -->
    <!-- Formulario de traducción -->
</body>
</html>

📊 2. El método getRecentList

Estructura del método

php
public function getRecentList() {
    // 1. Preparar la consulta
    $stmt = $this->DB->prepare("SELECT * FROM `files` ORDER BY `ID` DESC");
    
    // 2. Ejecutar
    $stmt->execute();
    
    // 3. Obtener resultados como objetos
    $files = $stmt->fetchAll(PDO::FETCH_OBJ);
    
    // 4. Generar el HTML
    foreach($files as $file) {
        // Mostrar cada archivo
    }
}

¿Qué hace PDO::FETCH_OBJ?

PDO::FETCH_OBJ devuelve cada fila como un objeto:

php
// Con PDO::FETCH_OBJ
$files = $stmt->fetchAll(PDO::FETCH_OBJ);
foreach($files as $file) {
    echo $file->ID;        // Acceso a propiedades
    echo $file->content;
    echo $file->fileUrl;
}

// Alternativa: PDO::FETCH_ASSOC (array asociativo)
$files = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach($files as $file) {
    echo $file['ID'];      // Acceso a array
    echo $file['content'];
}

Consulta SQL

sql
SELECT * FROM `files` ORDER BY `ID` DESC
ParteSignificado
SELECT *Selecciona todas las columnas
FROM filesDe la tabla files
ORDER BY ID DESCOrdena por ID descendente (más reciente primero)

🎨 3. Mostrando la lista de archivos

El HTML de cada elemento

html
<a href="view.php?file=[FILE-ID]">
    <li class="rounded flex my-5 cursor-pointer hover:bg-gray-100 items-center">
        <div class="w-14">
            <img src="frontend/images/[IMAGEN]"/>
        </div>
        <div class="overflow-hidden w-60 font-normal h-auto font-bold p-2">
            <div>
                <p class="truncate">[CONTENT]</p>
            </div>
        </div>
    </li>
</a>

Generando dinámicamente

php
foreach($files as $file) {
    echo '<a href="view.php?file=' . $file->ID . '">';
    echo '<li class="rounded flex my-5 cursor-pointer hover:bg-gray-100 items-center">';
    echo '<div class="w-14">';
    
    // Mostrar ícono según el tipo
    if ($file->type === 'audio') {
        echo '<img src="frontend/images/audio-img.png"/>';
    } else {
        echo '<img src="frontend/images/video-img.png"/>';
    }
    
    echo '</div>';
    echo '<div class="overflow-hidden w-60 font-normal h-auto font-bold p-2">';
    echo '<div><p class="truncate">' . $file->content . '</p></div>';
    echo '</div>';
    echo '</li>';
    echo '</a>';
}

🔀 4. Usando el operador ternario

¿Qué es el operador ternario?

Es una forma abreviada de escribir un if-else:

php
// Forma larga (if-else)
if ($file->type === 'audio') {
    echo '<img src="frontend/images/audio-img.png"/>';
} else {
    echo '<img src="frontend/images/video-img.png"/>';
}

// Forma corta (operador ternario)
echo ($file->type === 'audio') ? 
    '<img src="frontend/images/audio-img.png"/>' : 
    '<img src="frontend/images/video-img.png"/>';

Sintaxis

php
(condición) ? (valor_si_verdadero) : (valor_si_falso)

En nuestro código

php
echo '<div class="w-14">' .
     (($file->type === 'audio') ? 
         '<img src="frontend/images/audio-img.png"/>' : 
         '<img src="frontend/images/video-img.png"/>') .
     '</div>';

Ventajas del operador ternario

VentajaExplicación
ConcisoMenos líneas de código
LegibleFácil de leer para condiciones simples
In-lineSe puede usar dentro de echo

🗄️ 5. Corrigiendo la base de datos

Problema común

Si la columna content no existe en la tabla files, verás un error como:

text
Column 'content' not found in field list

Solución

Verificar la estructura de la tabla:

sql
DESCRIBE files;

Si falta la columna content:

sql
ALTER TABLE `files` 
ADD COLUMN `content` TEXT DEFAULT NULL AFTER `fileUrl`;

Estructura correcta:

text
+------------+----------------------+------+-----+---------+----------------+
| Field      | Type                 | Null | Key | Default | Extra          |
+------------+----------------------+------+-----+---------+----------------+
| ID         | int(11)              | NO   | PRI | NULL    | auto_increment |
| fileUrl    | varchar(255)         | NO   |     | NULL    |                |
| content    | text                 | YES  |     | NULL    |                |
| translated | text                 | YES  |     | NULL    |                |
| lang       | varchar(255)         | YES  |     | NULL    |                |
| type       | enum('audio','video')| NO   |     | NULL    |                |
+------------+----------------------+------+-----+---------+----------------+

💻 6. Código completo

Archivo: backend/classes/Whisper.php (método getRecentList 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'
                ];
            }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'){
                $this->file = curl_file_create($this->file);
            }
        }

        public function covert(){
            $apiUrl = $this->getApiUrl();
            $ch = curl_init($apiUrl);
            curl_setopt($ch, CURLOPT_POST, true);
            $this->getFile();
            curl_setopt($ch, CURLOPT_POSTFIELDS, $this->getData());
            curl_setopt($ch, CURLOPT_HTTPHEADER, $this->getHeader());
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            $response = curl_exec($ch);
            curl_close($ch);

            if($response){
                return json_decode($response, true);
            }else{
                $this->error = "API REQUEST FAILED";
            }
        }

        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 = "File is large!";
                }
            }else{
                $this->error = "invalid file format!";
            }
        }
        
        public function save($file, $content, $type){
            $stmt = $this->DB->prepare("INSERT INTO `files` (`fileUrl`, `content`, `type`) VALUES (:fileUrl, :content, :type)");
            $stmt->bindParam(":fileUrl", $file, PDO::PARAM_STR);
            $stmt->bindParam(":content", $content, PDO::PARAM_STR);
            $stmt->bindParam(":type", $type, PDO::PARAM_STR);
            $stmt->execute();
            return $this->DB->lastInsertId();
        }

        /**
         * Obtiene y muestra la lista de archivos recientes
         */
        public function getRecentList(){
            try {
                // 1. Preparar la consulta
                $stmt = $this->DB->prepare(
                    "SELECT * FROM `files` ORDER BY `ID` DESC"
                );
                
                // 2. Ejecutar
                $stmt->execute();
                
                // 3. Obtener resultados como objetos
                $files = $stmt->fetchAll(PDO::FETCH_OBJ);
                
                // 4. Generar la lista HTML
                foreach($files as $file){
                    // Enlace al archivo
                    echo '<a href="view.php?file=' . $file->ID . '">';
                    echo '<li class="rounded flex my-5 cursor-pointer hover:bg-gray-100 items-center">';
                    echo '<div class="w-14">';
                    
                    // Mostrar ícono según el tipo (operador ternario)
                    echo ($file->type === 'audio') ? 
                        '<img src="frontend/images/audio-img.png"/>' : 
                        '<img src="frontend/images/video-img.png"/>';
                    
                    echo '</div>';
                    echo '<div class="overflow-hidden w-60 font-normal h-auto font-bold p-2">';
                    echo '<div>';
                    
                    // Mostrar preview del contenido
                    $preview = strlen($file->content) > 50 ? 
                        substr($file->content, 0, 50) . '...' : 
                        $file->content;
                    
                    echo '<p class="truncate">' . htmlspecialchars($preview) . '</p>';
                    echo '</div>';
                    echo '</div>';
                    echo '</li>';
                    echo '</a>';
                }
                
            } catch (PDOException $e) {
                echo '<li class="text-red-500">Error al cargar archivos: ' . $e->getMessage() . '</li>';
            }
        }
    }
?>

Archivo: view.php (completo)

php
<?php 
    include 'backend/init.php';
    
    // Obtener el ID del archivo
    $fileId = isset($_GET['file']) ? (int)$_GET['file'] : 0;
    
    // Obtener los datos del archivo
    $fileData = null;
    if ($fileId > 0) {
        $stmt = $whisperObj->DB->prepare("SELECT * FROM `files` WHERE ID = :id");
        $stmt->bindParam(':id', $fileId, PDO::PARAM_INT);
        $stmt->execute();
        $fileData = $stmt->fetch(PDO::FETCH_OBJ);
    }
?>
<!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">
                <!--header-section-->
                <div class="flex w-full justify-between p-6">
                    <div class="flex justify-center flex-col">
                        <h2 class="text-2xl font-semibold">My Whisper</h2>
                        <span><a class="font-semibold text-lime-600" href="https://www.meralesson.com">Meralesson</a></span>
                    </div>
                    <div class="flex items-center justify-center">
                        <a href="index.php" class="border rounded px-4 py-2 cursor-pointer hover:bg-gray-100">Back</a>
                    </div>
                </div>
                <!--Body-section-->
                <div class="px-10 overflow-hidden">
                    <div class="flex">
                        <!--Left-video-section-->
                        <div class="w-1/3">
                            <div>
                                <!-- Audio Player -->
                                <?php if ($fileData && $fileData->type === 'audio'): ?>
                                <audio controls style="width:100%;border-radius: 20px;">
                                    <source src="<?php echo $fileData->fileUrl; ?>" type="audio/mpeg">
                                    Your browser does not support the audio element.
                                </audio>
                                <?php endif; ?>
                                
                                <!-- Video Player -->
                                <?php if ($fileData && $fileData->type === 'video'): ?>
                                <video style="height:240px; width:100%;" controls>
                                    <source src="<?php echo $fileData->fileUrl; ?>">
                                    Your browser does not support the video tag.
                                </video>
                                <?php endif; ?>
                            </div>
                            <!--Tabs-->
                            <div class="flex flex-col">
                                <div>
                                    <ul class="flex border-b justify-between">
                                        <li class="flex-1 flex font-sm items-center justify-center text-2xl text-gray-300 cursor-pointer hover:bg-gray-100">
                                            <span><i class="fa-solid fa-hands-asl-interpreting"></i></span>
                                        </li>
                                        <li class="flex-1 flex items-center justify-center font-sm text-2xl text-gray-300 cursor-pointer hover:bg-gray-100">
                                            <span><i class="fa-solid fa-database"></i></span>
                                        </li>
                                    </ul>
                                </div>
                                <div style="height: 400px; overflow:scroll;">
                                    <div class="flex">
                                        <ul class="w-full h-full">
                                            <!-- RECENT GENERATED FILES -->
                                            <?php $whisperObj->getRecentList(); ?>
                                        </ul>
                                    </div>
                                </div>
                            </div>
                        </div>
                        <!--Right-Transcript-section-->
                        <div class="flex-1">
                            <div id="scroll" class="py-10 px-5 overflow-y-scroll w-full" style="height:500px;">
                                
                                <ul class="w-full">
                                    <li class="flex flex-col my-5 w-full">
                                        <h3 class="text-2xl">Text</h3>
                                        <div>
                                            <?php 
                                            if ($fileData) {
                                                echo nl2br(htmlspecialchars($fileData->content));
                                            } else {
                                                echo "No file selected. Please select a file from the list.";
                                            }
                                            ?>
                                        </div>
                                    </li>
                                    <li class="flex flex-col my-5 w-full">
                                        <!--loader-->
                                        <div id="loader" class="hidden flex flex-col text-center flex-1 rounded-xl w-full">
                                            <div class="flex items-center flex-col">
                                                <img class="w-20 animate-pulse mt-20" src="frontend/images/loader.gif"/>
                                                <span class="animate-pulse">
                                                    Translating....
                                                </span>
                                            </div>
                                        </div>
                                    </li>
                                    <!-- TRANSLATED CONTENT -->
                                    <!-- <li id="translateContent" class="flex flex-col my-5 w-full">
                                        <h3 class="text-2xl">[LANG]</h3>
                                        <div>
                                            [TRANSLATED]
                                        </div>
                                    </li> -->
                                    
                                </ul>

                            </div>
                            <form id="form" method="POST">
                                <div class="flex flex-col justify-center items-center">
                                    <div class="">
                                        <label for="lang">Select Language</label>
                                        <select id="lang" name="lang" class="px-4 py-3 mx-2 my-3">
                                            <option value="English">English</option>
                                            <option value="French">French</option>
                                            <option value="Spanish">Spanish</option>
                                            <option value="German">German</option>
                                        </select>
                                        
                                        <button id="translateBtn" name="translate" class="border border-gray-700 rounded px-6 py-2 cursor-pointer hover:bg-gray-100 font-normal">Translate</button>
                                    </div>
                                    <!-- ERROR HERE -->
                                    <!-- <div class="flex bg-red-100 border border-red-400 text-red-700 px-4 py-2 rounded relative">
                                        <strong class="font-bold">Error:</strong>
                                        <span class="block sm:inline">[ERROR]</span>
                                    </div> -->
                                    
                                </div>
                            </form>
                        </div>
                    </div>
                </div>

            </div>
        </div>    
    </div>
</div>
<!-- JS CODE HERE -->
<script>
$(document).ready(function() {
    // Traducción
    $('#translateBtn').click(function(e) {
        e.preventDefault();
        $('#loader').removeClass('hidden');
        
        // Obtener idioma seleccionado
        var lang = $('#lang').val();
        var content = $('div#scroll h3:first').next('div').text();
        var fileId = <?php echo $fileId; ?>;
        
        if (content && content !== 'No file selected.') {
            $.ajax({
                url: 'translate.php',
                method: 'POST',
                data: {
                    content: content,
                    lang: lang,
                    file_id: fileId
                },
                success: function(response) {
                    $('#loader').addClass('hidden');
                    // Aquí se mostraría el resultado de la traducción
                    alert('Traducción completada');
                },
                error: function() {
                    $('#loader').addClass('hidden');
                    alert('Error al traducir');
                }
            });
        } else {
            $('#loader').addClass('hidden');
            alert('No hay contenido para traducir');
        }
    });
});
</script>
</body>
</html>

📖 7. Explicación detallada

El método getRecentList paso a paso

php
public function getRecentList() {
    // 1. Preparar y ejecutar la consulta
    $stmt = $this->DB->prepare(
        "SELECT * FROM `files` ORDER BY `ID` DESC"
    );
    $stmt->execute();
    
    // 2. Obtener resultados como objetos
    $files = $stmt->fetchAll(PDO::FETCH_OBJ);
    
    // 3. Iterar sobre cada archivo
    foreach($files as $file) {
        // 4. Generar el HTML
    }
}

¿Qué hace htmlspecialchars()?

htmlspecialchars() convierte caracteres especiales en entidades HTML:

php
$text = "Hello & welcome <script>";
echo htmlspecialchars($text);
// Resultado: Hello &amp; welcome &lt;script&gt;

¿Por qué es importante?

  • Previene ataques XSS (Cross-Site Scripting)

  • Los caracteres especiales se muestran correctamente

¿Qué hace nl2br()?

nl2br() convierte saltos de línea en <br> HTML:

php
$text = "Línea 1\nLínea 2";
echo nl2br($text);
// Resultado: Línea 1<br>Línea 2

🧪 8. Probando la aplicación

Paso 1: Verificar la base de datos

sql
-- Verificar la estructura
DESCRIBE files;

-- Verificar que hay datos
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 la lista

  1. Después de procesar, serás redirigido a view.php

  2. En el panel izquierdo, verás la lista de archivos

  3. Cada archivo muestra un preview de la transcripción

Paso 4: Ver el contenido

  1. Haz clic en un archivo de la lista

  2. El reproductor mostrará el audio/video

  3. La transcripción se mostrará en el panel derecho


📊 9. Resumen del flujo

Diagrama de navegación

text
index.php
    ↓
[Upload File]
    ↓
[Procesar con Whisper]
    ↓
[Guardar en BD]
    ↓
view.php?file=ID
    ↓
[Mostrar archivo]
    ↓
[Mostrar lista de archivos]

Tabla de archivos mostrados

ColumnaUso en la vista
IDEnlace: view.php?file=ID
fileUrlReproductor de audio/video
contentTranscripción mostrada
typeÍcono (audio/video)

❓ Preguntas frecuentes

¿Por qué usar PDO::FETCH_OBJ?

  • Devuelve objetos, permitiendo acceso con ->

  • Más limpio que arrays asociativos

¿Qué es htmlspecialchars() y por qué usarlo?

  • Escapa caracteres HTML para prevenir XSS

  • Convierte &, <, >, " y '

¿Por qué usar nl2br()?

  • Convierte saltos de línea a HTML

  • Mejora la legibilidad del texto

¿Qué es el operador ternario?

  • Una forma abreviada de if-else

  • Útil para condiciones simples en echo

¿Cómo funciona la redirección en view.php?

  • header("location: view.php?file={$fileID}")

  • Redirige al archivo recién creado

¿Por qué separar la lógica en un método?

  • Reutilización: se usa en varias páginas

  • Mantenimiento: cambios en un solo lugar


🎯 Próximos pasos

Lo que hemos logrado

✅ Página view.php creada
✅ Método getRecentList implementado
✅ Lista de archivos recientes mostrada
✅ Reproductor de audio/video
✅ Visualización de transcripciones

Lo que viene en la próxima lección

En la siguiente lección vamos a:

  1. Implementar la traducción del texto

  2. Usar la API de ChatGPT para traducir

  3. Guardar la traducción en la base de datos

  4. Mostrar la traducción al usuario

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

php
// translate.php
<?php
include 'backend/init.php';

$content = $_POST['content'] ?? '';
$lang = $_POST['lang'] ?? 'Spanish';
$fileId = $_POST['file_id'] ?? 0;

if ($content && $fileId) {
    $whisperObj->dataType = 'TRANSLATE';
    $whisperObj->content = $content;
    $whisperObj->lang = $lang;
    $result = $whisperObj->convert();
    
    if ($result) {
        $translated = $result['choices'][0]['message']['content'];
        // Guardar en la base de datos
        $whisperObj->saveTranslation($fileId, $translated, $lang);
        echo json_encode(['success' => true, 'translated' => $translated]);
    }
}
?>

¡Excelente trabajo! Ahora tenemos una página completa para ver los archivos generados recientemente, con reproductor y lista de archivos. En la próxima lección, añadiremos la funcionalidad de traducción.

Comentarios

Entradas más populares de este blog

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

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