Aplicación PHP + MySQL para guardar transcripciones de Whisper
Aplicación PHP + MySQL para guardar transcripciones de Whisper
¡Excelente idea! Te voy a mostrar cómo crear una aplicación web completa para gestionar tus transcripciones de Whisper. Podrás subir videos, transcribirlos automáticamente (usando Whisper en el servidor) y guardar todo en una base de datos MySQL.
📁 Estructura del Proyecto
whisper-app/ ├── config/ │ └── database.php ├── includes/ │ ├── header.php │ └── footer.php ├── assets/ │ ├── css/ │ │ └── style.css │ └── uploads/ ├── index.php ├── upload.php ├── transcribe.php ├── view.php ├── edit.php ├── delete.php └── export.php
🗄️ Configuración de la Base de Datos
1. Crear la base de datos y la tabla
Ejecuta este SQL en phpMyAdmin o en tu cliente MySQL:
CREATE DATABASE whisper_app CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE whisper_app; CREATE TABLE transcriptions ( id INT AUTO_INCREMENT PRIMARY KEY, video_name VARCHAR(255) NOT NULL, video_path VARCHAR(500) NOT NULL, text TEXT NOT NULL, srt_subtitles TEXT, word_count INT DEFAULT 0, language VARCHAR(50) DEFAULT 'Spanish', model_used VARCHAR(50) DEFAULT 'base', duration INT DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_created (created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
🔧 Archivos de Configuración
2. config/database.php
<?php // Configuración de la base de datos define('DB_HOST', 'localhost'); define('DB_NAME', 'whisper_app'); define('DB_USER', 'root'); define('DB_PASS', ''); // Configuración de la aplicación define('APP_NAME', 'Whisper Transcriber'); define('UPLOAD_DIR', __DIR__ . '/../assets/uploads/'); define('MAX_FILE_SIZE', 500 * 1024 * 1024); // 500 MB define('ALLOWED_EXTENSIONS', ['mp4', 'mkv', 'avi', 'mov', 'mp3', 'wav', 'm4a']); // Intentar conectar a la base de datos try { $pdo = new PDO("mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4", DB_USER, DB_PASS); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); } catch(PDOException $e) { die("Error de conexión: " . $e->getMessage()); } // Crear directorio de uploads si no existe if (!is_dir(UPLOAD_DIR)) { mkdir(UPLOAD_DIR, 0777, true); } ?>
🎨 Archivos de Plantilla
3. includes/header.php
<!DOCTYPE html> <html lang="es"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title><?php echo APP_NAME; ?></title> <link rel="stylesheet" href="assets/css/style.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> </head> <body> <nav class="navbar"> <div class="container"> <a href="index.php" class="navbar-brand"> <i class="fas fa-microphone"></i> <?php echo APP_NAME; ?> </a> <div class="navbar-nav"> <a href="index.php" class="nav-link"><i class="fas fa-home"></i> Inicio</a> <a href="upload.php" class="nav-link"><i class="fas fa-upload"></i> Subir Video</a> </div> </div> </nav> <main class="container">
4. includes/footer.php
</main> <footer class="footer"> <div class="container"> <p>© <?php echo date('Y'); ?> <?php echo APP_NAME; ?> - Hecho con ❤️ usando Whisper</p> </div> </footer> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> <script src="assets/js/main.js"></script> </body> </html>
🎯 Página Principal
5. index.php
<?php require_once 'config/database.php'; include 'includes/header.php'; // Obtener todas las transcripciones $query = "SELECT * FROM transcriptions ORDER BY created_at DESC"; $stmt = $pdo->query($query); $transcriptions = $stmt->fetchAll(); ?> <div class="row"> <div class="col-12"> <div class="card"> <div class="card-header"> <h2><i class="fas fa-list"></i> Transcripciones Guardadas</h2> <a href="upload.php" class="btn btn-primary"> <i class="fas fa-plus"></i> Nueva Transcripción </a> </div> <?php if (empty($transcriptions)): ?> <div class="empty-state"> <i class="fas fa-inbox" style="font-size: 48px;"></i> <h3>No hay transcripciones</h3> <p>Sube tu primer video para comenzar</p> <a href="upload.php" class="btn btn-primary">Subir Video</a> </div> <?php else: ?> <div class="table-responsive"> <table class="table"> <thead> <tr> <th>ID</th> <th>Video</th> <th>Palabras</th> <th>Idioma</th> <th>Fecha</th> <th>Acciones</th> </tr> </thead> <tbody> <?php foreach ($transcriptions as $item): ?> <tr> <td>#<?php echo $item['id']; ?></td> <td> <strong><?php echo htmlspecialchars($item['video_name']); ?></strong><br> <small class="text-muted">Modelo: <?php echo $item['model_used']; ?></small> </td> <td><?php echo number_format($item['word_count']); ?></td> <td><span class="badge"><?php echo $item['language']; ?></span></td> <td><?php echo date('d/m/Y H:i', strtotime($item['created_at'])); ?></td> <td> <div class="btn-group"> <a href="view.php?id=<?php echo $item['id']; ?>" class="btn btn-sm btn-info"> <i class="fas fa-eye"></i> </a> <a href="edit.php?id=<?php echo $item['id']; ?>" class="btn btn-sm btn-warning"> <i class="fas fa-edit"></i> </a> <a href="export.php?id=<?php echo $item['id']; ?>&format=txt" class="btn btn-sm btn-success"> <i class="fas fa-download"></i> </a> <a href="delete.php?id=<?php echo $item['id']; ?>" class="btn btn-sm btn-danger" onclick="return confirm('¿Estás seguro de eliminar esta transcripción?')"> <i class="fas fa-trash"></i> </a> </div> </td> </tr> <?php endforeach; ?> </tbody> </table> </div> <?php endif; ?> </div> </div> </div> <?php include 'includes/footer.php'; ?>
📤 Subir y Transcribir
6. upload.php
<?php require_once 'config/database.php'; include 'includes/header.php'; $message = ''; $error = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['video'])) { $file = $_FILES['video']; $extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); // Validar extensión if (!in_array($extension, ALLOWED_EXTENSIONS)) { $error = "Formato de archivo no permitido. Extensiones permitidas: " . implode(', ', ALLOWED_EXTENSIONS); } // Validar tamaño elseif ($file['size'] > MAX_FILE_SIZE) { $error = "El archivo excede el tamaño máximo de " . (MAX_FILE_SIZE / 1024 / 1024) . " MB"; } // Validar errores de subida elseif ($file['error'] !== UPLOAD_ERR_OK) { $error = "Error al subir el archivo: " . $file['error']; } else { // Generar nombre único $filename = uniqid() . '.' . $extension; $filepath = UPLOAD_DIR . $filename; if (move_uploaded_file($file['tmp_name'], $filepath)) { // Redirigir a transcripción header("Location: transcribe.php?file=" . urlencode($filename) . "&original_name=" . urlencode($file['name'])); exit; } else { $error = "Error al mover el archivo al servidor"; } } } ?> <div class="row"> <div class="col-md-8 offset-md-2"> <div class="card"> <div class="card-header"> <h2><i class="fas fa-upload"></i> Subir Video/Audio</h2> </div> <div class="card-body"> <?php if ($error): ?> <div class="alert alert-danger"><?php echo $error; ?></div> <?php endif; ?> <?php if ($message): ?> <div class="alert alert-success"><?php echo $message; ?></div> <?php endif; ?> <form action="" method="POST" enctype="multipart/form-data" id="uploadForm"> <div class="form-group"> <label for="video" class="drop-zone"> <i class="fas fa-cloud-upload-alt" style="font-size: 48px;"></i> <h3>Arrastra tu archivo aquí</h3> <p>o haz clic para seleccionar</p> <small>Formatos: <?php echo implode(', ', ALLOWED_EXTENSIONS); ?> | Máx: <?php echo MAX_FILE_SIZE / 1024 / 1024; ?> MB</small> <input type="file" name="video" id="video" accept="<?php echo '.' . implode(',.', ALLOWED_EXTENSIONS); ?>" required> </label> </div> <div class="form-group" id="fileInfo" style="display:none;"> <div class="file-info"> <i class="fas fa-file-video"></i> <span id="fileName"></span> <span id="fileSize"></span> </div> </div> <button type="submit" class="btn btn-primary btn-block"> <i class="fas fa-upload"></i> Subir y Transcribir </button> </form> </div> </div> <div class="card mt-4"> <div class="card-body"> <h5><i class="fas fa-info-circle"></i> Consejos</h5> <ul> <li>Whisper funciona mejor con audio en español</li> <li>Para mejores resultados, usa archivos con audio claro</li> <li>La transcripción puede tomar varios minutos</li> </ul> </div> </div> </div> </div> <script> document.getElementById('video').addEventListener('change', function(e) { const file = this.files[0]; if (file) { document.getElementById('fileInfo').style.display = 'block'; document.getElementById('fileName').textContent = file.name; document.getElementById('fileSize').textContent = (file.size / 1024 / 1024).toFixed(2) + ' MB'; } }); // Drag and drop const dropZone = document.querySelector('.drop-zone'); dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.style.borderColor = '#007bff'; }); dropZone.addEventListener('dragleave', () => { dropZone.style.borderColor = '#ddd'; }); dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.style.borderColor = '#ddd'; const files = e.dataTransfer.files; if (files.length > 0) { document.getElementById('video').files = files; document.getElementById('video').dispatchEvent(new Event('change')); } }); </script> <?php include 'includes/footer.php'; ?>
🤖 Procesar Transcripción con Whisper
7. transcribe.php
<?php require_once 'config/database.php'; // Verificar que se recibió el archivo if (!isset($_GET['file']) || !isset($_GET['original_name'])) { header('Location: index.php'); exit; } $filename = $_GET['file']; $original_name = $_GET['original_name']; $filepath = UPLOAD_DIR . $filename; // Verificar que el archivo existe if (!file_exists($filepath)) { die("El archivo no existe en el servidor"); } // Detectar el modelo a usar (puedes cambiar esto) $model = 'small'; // Opciones: tiny, base, small, medium, large $language = 'Spanish'; // Ejecutar Whisper $output_dir = UPLOAD_DIR; $base_name = pathinfo($filename, PATHINFO_FILENAME); $output_file = $output_dir . $base_name . '.txt'; $srt_file = $output_dir . $base_name . '.srt'; // Construir comando para Whisper $command = "whisper " . escapeshellarg($filepath) . " --model " . $model . " --language " . $language . " --output_dir " . escapeshellarg($output_dir) . " 2>&1"; // Ejecutar y capturar la salida exec($command, $output, $return_code); // Leer el resultado $text = ''; $srt_content = ''; if (file_exists($output_file)) { $text = file_get_contents($output_file); } if (file_exists($srt_file)) { $srt_content = file_get_contents($srt_file); } // Calcular estadísticas básicas $word_count = str_word_count($text); // Guardar en la base de datos try { $sql = "INSERT INTO transcriptions (video_name, video_path, text, srt_subtitles, word_count, language, model_used) VALUES (?, ?, ?, ?, ?, ?, ?)"; $stmt = $pdo->prepare($sql); $stmt->execute([ $original_name, $filename, $text, $srt_content, $word_count, $language, $model ]); $id = $pdo->lastInsertId(); // Redirigir a la vista header("Location: view.php?id=" . $id); exit; } catch (PDOException $e) { die("Error al guardar en la base de datos: " . $e->getMessage()); } ?>
👁️ Ver Transcripción
8. view.php
<?php require_once 'config/database.php'; include 'includes/header.php'; if (!isset($_GET['id'])) { header('Location: index.php'); exit; } $id = (int)$_GET['id']; // Obtener la transcripción $sql = "SELECT * FROM transcriptions WHERE id = ?"; $stmt = $pdo->prepare($sql); $stmt->execute([$id]); $item = $stmt->fetch(); if (!$item) { header('Location: index.php'); exit; } ?> <div class="row"> <div class="col-12"> <div class="card"> <div class="card-header"> <h2><i class="fas fa-file-alt"></i> Transcripción: <?php echo htmlspecialchars($item['video_name']); ?></h2> <div> <a href="edit.php?id=<?php echo $id; ?>" class="btn btn-warning btn-sm"> <i class="fas fa-edit"></i> Editar </a> <a href="export.php?id=<?php echo $id; ?>&format=txt" class="btn btn-success btn-sm"> <i class="fas fa-download"></i> TXT </a> <a href="export.php?id=<?php echo $id; ?>&format=srt" class="btn btn-info btn-sm"> <i class="fas fa-download"></i> SRT </a> <a href="index.php" class="btn btn-secondary btn-sm">Volver</a> </div> </div> <div class="card-body"> <div class="row mb-4"> <div class="col-md-3"> <strong><i class="fas fa-video"></i> Video:</strong> <p><?php echo htmlspecialchars($item['video_name']); ?></p> </div> <div class="col-md-3"> <strong><i class="fas fa-language"></i> Idioma:</strong> <p><?php echo $item['language']; ?></p> </div> <div class="col-md-3"> <strong><i class="fas fa-robot"></i> Modelo:</strong> <p><?php echo $item['model_used']; ?></p> </div> <div class="col-md-3"> <strong><i class="fas fa-clock"></i> Creado:</strong> <p><?php echo date('d/m/Y H:i', strtotime($item['created_at'])); ?></p> </div> </div> <h4>Texto completo</h4> <div class="transcription-text"> <?php echo nl2br(htmlspecialchars($item['text'])); ?> </div> <?php if (!empty($item['srt_subtitles'])): ?> <hr> <h4>Subtítulos (SRT)</h4> <div class="transcription-text"> <pre><?php echo htmlspecialchars($item['srt_subtitles']); ?></pre> </div> <?php endif; ?> </div> </div> </div> </div> <?php include 'includes/footer.php'; ?>
✏️ Editar Transcripción
9. edit.php
<?php require_once 'config/database.php'; include 'includes/header.php'; if (!isset($_GET['id'])) { header('Location: index.php'); exit; } $id = (int)$_GET['id']; $message = ''; $error = ''; // Obtener la transcripción $sql = "SELECT * FROM transcriptions WHERE id = ?"; $stmt = $pdo->prepare($sql); $stmt->execute([$id]); $item = $stmt->fetch(); if (!$item) { header('Location: index.php'); exit; } if ($_SERVER['REQUEST_METHOD'] === 'POST') { $text = trim($_POST['text']); if (empty($text)) { $error = "El texto no puede estar vacío"; } else { $word_count = str_word_count($text); $update_sql = "UPDATE transcriptions SET text = ?, word_count = ?, updated_at = NOW() WHERE id = ?"; $update_stmt = $pdo->prepare($update_sql); if ($update_stmt->execute([$text, $word_count, $id])) { $message = "Transcripción actualizada correctamente"; // Recargar datos $stmt->execute([$id]); $item = $stmt->fetch(); } else { $error = "Error al actualizar"; } } } ?> <div class="row"> <div class="col-12"> <div class="card"> <div class="card-header"> <h2><i class="fas fa-edit"></i> Editar Transcripción</h2> </div> <div class="card-body"> <?php if ($message): ?> <div class="alert alert-success"><?php echo $message; ?></div> <?php endif; ?> <?php if ($error): ?> <div class="alert alert-danger"><?php echo $error; ?></div> <?php endif; ?> <form action="" method="POST"> <div class="form-group"> <label><strong>Video:</strong> <?php echo htmlspecialchars($item['video_name']); ?></label> </div> <div class="form-group"> <label for="text"><strong>Texto</strong></label> <textarea name="text" id="text" class="form-control" rows="15" required><?php echo htmlspecialchars($item['text']); ?></textarea> </div> <div class="form-group"> <button type="submit" class="btn btn-primary"> <i class="fas fa-save"></i> Guardar Cambios </button> <a href="view.php?id=<?php echo $id; ?>" class="btn btn-secondary">Cancelar</a> </div> </form> </div> </div> </div> </div> <?php include 'includes/footer.php'; ?>
📥 Exportar Transcripción
10. export.php
<?php require_once 'config/database.php'; if (!isset($_GET['id']) || !isset($_GET['format'])) { header('Location: index.php'); exit; } $id = (int)$_GET['id']; $format = $_GET['format']; // Obtener la transcripción $sql = "SELECT * FROM transcriptions WHERE id = ?"; $stmt = $pdo->prepare($sql); $stmt->execute([$id]); $item = $stmt->fetch(); if (!$item) { header('Location: index.php'); exit; } $filename = pathinfo($item['video_name'], PATHINFO_FILENAME); $content = ''; $content_type = 'text/plain'; $extension = 'txt'; if ($format === 'srt') { $content = $item['srt_subtitles'] ?? "No hay subtítulos disponibles"; $extension = 'srt'; } else { // Formato TXT por defecto $content = $item['text']; $extension = 'txt'; } // Forzar descarga header('Content-Type: ' . $content_type); header('Content-Disposition: attachment; filename="' . $filename . '.' . $extension . '"'); header('Content-Length: ' . strlen($content)); header('Cache-Control: private, max-age=0, must-revalidate'); header('Pragma: public'); echo $content; exit; ?>
🗑️ Eliminar Transcripción
11. delete.php
<?php require_once 'config/database.php'; if (!isset($_GET['id'])) { header('Location: index.php'); exit; } $id = (int)$_GET['id']; // Obtener información del archivo $sql = "SELECT video_path FROM transcriptions WHERE id = ?"; $stmt = $pdo->prepare($sql); $stmt->execute([$id]); $item = $stmt->fetch(); if ($item) { // Eliminar archivos físicos $filepath = UPLOAD_DIR . $item['video_path']; if (file_exists($filepath)) { unlink($filepath); } // Eliminar archivos de texto generados $base_name = pathinfo($item['video_path'], PATHINFO_FILENAME); $files_to_delete = [ UPLOAD_DIR . $base_name . '.txt', UPLOAD_DIR . $base_name . '.srt', UPLOAD_DIR . $base_name . '.vtt', UPLOAD_DIR . $base_name . '.json' ]; foreach ($files_to_delete as $file) { if (file_exists($file)) { unlink($file); } } // Eliminar de la base de datos $delete_sql = "DELETE FROM transcriptions WHERE id = ?"; $delete_stmt = $pdo->prepare($delete_sql); $delete_stmt->execute([$id]); } header('Location: index.php'); exit; ?>
🎨 Estilos CSS
12. assets/css/style.css
* { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background: #f5f7fa; color: #333; display: flex; flex-direction: column; min-height: 100vh; } .container { max-width: 1200px; margin: 0 auto; padding: 0 20px; width: 100%; } /* Navbar */ .navbar { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 15px 0; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } .navbar .container { display: flex; justify-content: space-between; align-items: center; } .navbar-brand { color: white; text-decoration: none; font-size: 1.5rem; font-weight: bold; } .navbar-brand i { margin-right: 10px; } .nav-link { color: rgba(255,255,255,0.8); text-decoration: none; padding: 8px 15px; border-radius: 5px; transition: all 0.3s; } .nav-link:hover { color: white; background: rgba(255,255,255,0.1); } .navbar-nav { display: flex; gap: 10px; } /* Main content */ main { flex: 1; padding: 30px 0; } /* Cards */ .card { background: white; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); overflow: hidden; margin-bottom: 30px; } .card-header { padding: 20px 25px; border-bottom: 1px solid #e9ecef; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; } .card-header h2 { font-size: 1.25rem; margin: 0; display: flex; align-items: center; gap: 10px; } .card-body { padding: 25px; } /* Tables */ .table-responsive { overflow-x: auto; } .table { width: 100%; border-collapse: collapse; } .table th { background: #f8f9fa; padding: 12px 15px; text-align: left; font-weight: 600; font-size: 0.9rem; border-bottom: 2px solid #dee2e6; } .table td { padding: 12px 15px; border-bottom: 1px solid #e9ecef; vertical-align: middle; } .table tr:hover { background: #f8f9fa; } /* Buttons */ .btn { display: inline-block; padding: 8px 16px; border-radius: 5px; border: none; cursor: pointer; text-decoration: none; font-size: 0.9rem; transition: all 0.3s; font-weight: 500; } .btn-primary { background: #667eea; color: white; } .btn-primary:hover { background: #5a67d8; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); } .btn-success { background: #48bb78; color: white; } .btn-success:hover { background: #38a169; } .btn-info { background: #4fd1c5; color: white; } .btn-info:hover { background: #38b2ac; } .btn-warning { background: #ed8936; color: white; } .btn-warning:hover { background: #dd6b20; } .btn-danger { background: #fc8181; color: white; } .btn-danger:hover { background: #f56565; } .btn-secondary { background: #a0aec0; color: white; } .btn-secondary:hover { background: #718096; } .btn-block { width: 100%; padding: 12px; } .btn-sm { padding: 5px 10px; font-size: 0.8rem; } .btn-group { display: flex; gap: 5px; flex-wrap: wrap; } /* Forms */ .form-group { margin-bottom: 20px; } .form-group label { display: block; margin-bottom: 5px; font-weight: 500; } .form-control { width: 100%; padding: 10px 15px; border: 1px solid #d1d5db; border-radius: 5px; font-size: 1rem; transition: border-color 0.3s; } .form-control:focus { outline: none; border-color: #667eea; box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); } textarea.form-control { font-family: 'Courier New', monospace; resize: vertical; } /* Drop zone */ .drop-zone { display: block; padding: 60px 20px; text-align: center; border: 2px dashed #d1d5db; border-radius: 10px; cursor: pointer; transition: all 0.3s; background: #fafafa; } .drop-zone:hover { border-color: #667eea; background: #f7f7ff; } .drop-zone i { color: #667eea; } .drop-zone h3 { margin: 15px 0 5px; font-size: 1.2rem; } .drop-zone p { margin: 5px 0; color: #6b7280; } .drop-zone small { color: #9ca3af; } .drop-zone input[type="file"] { display: none; } /* Badge */ .badge { display: inline-block; padding: 3px 10px; background: #e9ecef; border-radius: 20px; font-size: 0.8rem; font-weight: 500; } /* Alerts */ .alert { padding: 15px 20px; border-radius: 5px; margin-bottom: 20px; } .alert-success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; } .alert-danger { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; } /* Empty state */ .empty-state { text-align: center; padding: 60px 20px; } .empty-state i { color: #d1d5db; margin-bottom: 20px; } .empty-state h3 { margin-bottom: 10px; color: #374151; } .empty-state p { color: #6b7280; margin-bottom: 20px; } /* File info */ .file-info { display: flex; align-items: center; gap: 10px; padding: 10px 15px; background: #f8f9fa; border-radius: 5px; } .file-info i { color: #667eea; font-size: 1.5rem; } /* Transcription text */ .transcription-text { background: #f8f9fa; padding: 20px; border-radius: 5px; max-height: 500px; overflow-y: auto; white-space: pre-wrap; word-wrap: break-word; font-family: 'Courier New', monospace; font-size: 0.95rem; line-height: 1.6; } .transcription-text pre { background: transparent; border: none; padding: 0; margin: 0; font-family: inherit; } /* Footer */ .footer { background: white; padding: 20px 0; margin-top: auto; border-top: 1px solid #e9ecef; text-align: center; color: #6b7280; } /* Responsive */ @media (max-width: 768px) { .navbar .container { flex-direction: column; gap: 10px; } .card-header { flex-direction: column; align-items: flex-start; } .table th, .table td { padding: 8px 10px; font-size: 0.85rem; } .btn-group { flex-direction: column; } .btn-group .btn { width: 100%; } .col-md-3, .col-md-8, .col-md-6 { width: 100%; } } /* Grid */ .row { display: flex; flex-wrap: wrap; margin: 0 -15px; } .col-12 { width: 100%; padding: 0 15px; } .col-md-3 { width: 25%; padding: 0 15px; } .col-md-6 { width: 50%; padding: 0 15px; } .col-md-8 { width: 66.666%; padding: 0 15px; } @media (max-width: 768px) { .col-md-3, .col-md-6, .col-md-8 { width: 100%; } } .mt-4 { margin-top: 2rem; } .mb-4 { margin-bottom: 2rem; } .mr-2 { margin-right: 0.5rem; }
🚀 Instalación Rápida
1. Requisitos del Servidor
# Instalar PHP y MySQL (en Ubuntu/Debian) sudo apt update sudo apt install php php-mysql php-mbstring php-xml php-curl php-zip php-gd php-intl sudo apt install mysql-server # Instalar Python y Whisper sudo apt install python3 python3-pip ffmpeg pip3 install openai-whisper
2. Configurar la Base de Datos
sudo mysql -u root -p # Ejecuta el SQL de creación de la base de datos
3. Configurar el Proyecto
# Copia los archivos al servidor web sudo cp -r whisper-app /var/www/html/ sudo chown -R www-data:www-data /var/www/html/whisper-app/assets/uploads/ sudo chmod 755 /var/www/html/whisper-app/assets/uploads/
4. Configurar Apache/Nginx
Asegúrate de que el directorio raíz apunte a la carpeta del proyecto.
5. ¡Listo!
Visita http://localhost/whisper-app/index.php y comienza a transcribir.
🔒 Mejoras de Seguridad Sugeridas
// Agregar estas funciones a config/database.php // Sanitizar entradas function sanitize($input) { return htmlspecialchars(strip_tags(trim($input)), ENT_QUOTES, 'UTF-8'); } // Validar archivos function isVideoFile($filename) { $allowed = ['mp4', 'mkv', 'avi', 'mov', 'webm']; $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); return in_array($ext, $allowed); } // Limitar acceso (agregar autenticación básica) function requireLogin() { if (!isset($_SESSION['user_id'])) { header('Location: login.php'); exit; } }
¡Y eso es todo! Ahora tienes una aplicación completa para gestionar tus transcripciones de Whisper. La interfaz es intuitiva, y puedes expandirla fácilmente añadiendo más funciones como búsqueda, categorías, o incluso un reproductor de video incrustado.
Comentarios
Publicar un comentario