UNPKG

@personnn/personnnkit

Version:

🇵 PersonnnKit - El Agente Kit Universal. Framework revolucionario para crear agentes de IA con HTML + Python. Simplicidad radical vs frameworks gigantes.

941 lines (775 loc) 32 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.youtubeAgent = void 0; exports.youtubeAgent = { id: 'youtube', name: 'YouTube Content Extractor', description: 'Agente especializado en extraer y analizar contenido de YouTube usando Whisper AI', category: 'content', dependencies: [ 'fastapi', 'uvicorn', 'yt-dlp', 'openai-whisper', 'requests', 'python-dotenv', 'pytest' ], scripts: { 'start': 'npm run dev', 'dev': 'node runtime/dev-server.js', 'build': 'node runtime/build.js', 'test': 'python -m pytest tests/ -v', 'install': 'bash scripts/install.sh', 'setup': 'bash scripts/install.sh', 'extract': 'python scripts/youtube_extractor.py' }, files: { 'pages/index.html': `<!DOCTYPE html> <html lang="es"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>YouTube Content Extractor - PersonnnKit</title> <link href="./css/tailwind.css" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet"> <style> :root { --primary: #ff0000; --primary-dark: #cc0000; --secondary: #282828; --accent: #00ff00; } body { font-family: 'Inter', sans-serif; background: linear-gradient(135deg, #0f0f23 0%, #1a1a2e 50%, #16213e 100%); color: white; } .glass { background: rgba(255, 255, 255, 0.05); backdrop-filter: blur(20px); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 16px; } .youtube-red { background: linear-gradient(135deg, var(--primary), var(--primary-dark)); } .input-group { position: relative; margin-bottom: 1rem; } .input-group input { width: 100%; padding: 12px 16px; background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 8px; color: white; font-size: 16px; } .input-group input::placeholder { color: rgba(255, 255, 255, 0.6); } .btn { padding: 12px 24px; border: none; border-radius: 8px; font-weight: 600; cursor: pointer; transition: all 0.3s ease; text-decoration: none; display: inline-block; } .btn:hover { transform: translateY(-2px); box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3); } .progress-bar { width: 100%; height: 8px; background: rgba(255, 255, 255, 0.1); border-radius: 4px; overflow: hidden; margin: 16px 0; } .progress-fill { height: 100%; background: linear-gradient(90deg, var(--primary), var(--accent)); width: 0%; transition: width 0.3s ease; } </style> </head> <body class="min-h-screen"> <div class="container mx-auto px-4 py-8"> <div class="max-w-4xl mx-auto"> <!-- Header --> <div class="text-center mb-12"> <h1 class="text-5xl font-bold mb-4"> <span style="color: var(--primary);">📺</span> YouTube Content Extractor </h1> <p class="text-xl text-gray-300">Extrae y analiza contenido de YouTube usando IA</p> </div> <!-- Main Interface --> <div class="glass p-8 mb-8"> <h2 class="text-2xl font-semibold mb-6">🎯 Extraer Contenido</h2> <div class="input-group"> <input type="url" id="youtube-url" placeholder="https://www.youtube.com/watch?v=..." class="w-full" > </div> <div class="flex gap-4 mb-6"> <button onclick="extractContent()" class="btn youtube-red text-white flex-1"> 🚀 Extraer Contenido </button> <button onclick="clearResults()" class="btn glass text-white"> 🗑️ Limpiar </button> </div> <div id="progress-container" class="hidden"> <div class="progress-bar"> <div id="progress-fill" class="progress-fill"></div> </div> <p id="progress-text" class="text-center text-sm text-gray-400">Preparando...</p> </div> </div> <!-- Results --> <div id="results-container" class="hidden"> <div class="glass p-6 mb-6"> <h3 class="text-xl font-semibold mb-4">📊 Información del Video</h3> <div id="video-info" class="grid md:grid-cols-2 gap-4"></div> </div> <div class="glass p-6 mb-6"> <h3 class="text-xl font-semibold mb-4">📝 Transcripción</h3> <div id="transcription" class="bg-black/30 p-4 rounded-lg max-h-96 overflow-y-auto"></div> </div> <div class="glass p-6"> <h3 class="text-xl font-semibold mb-4">🧠 Análisis de Contenido</h3> <div id="analysis" class="grid md:grid-cols-3 gap-4"></div> </div> </div> <!-- Output Console --> <div class="glass p-6"> <h3 class="text-xl font-semibold mb-4">💻 Console Output</h3> <div id="console-output" class="bg-black/50 p-4 rounded-lg min-h-32 font-mono text-sm"> <div class="text-green-400">✅ YouTube Content Extractor listo...</div> </div> </div> </div> </div> <script> let currentProgress = 0; function updateProgress(percent, text) { const progressContainer = document.getElementById('progress-container'); const progressFill = document.getElementById('progress-fill'); const progressText = document.getElementById('progress-text'); progressContainer.classList.remove('hidden'); progressFill.style.width = percent + '%'; progressText.textContent = text; if (percent >= 100) { setTimeout(() => { progressContainer.classList.add('hidden'); }, 2000); } } function logToConsole(message, type = 'info') { const console = document.getElementById('console-output'); const timestamp = new Date().toLocaleTimeString(); const colors = { info: 'text-blue-400', success: 'text-green-400', warning: 'text-yellow-400', error: 'text-red-400' }; const logEntry = document.createElement('div'); logEntry.className = colors[type] || colors.info; logEntry.innerHTML = \`[\${timestamp}] \${message}\`; console.appendChild(logEntry); console.scrollTop = console.scrollHeight; } async function extractContent() { const urlInput = document.getElementById('youtube-url'); const url = urlInput.value.trim(); if (!url) { logToConsole('❌ Por favor ingresa una URL de YouTube', 'error'); return; } if (!url.includes('youtube.com') && !url.includes('youtu.be')) { logToConsole('❌ URL inválida. Debe ser de YouTube', 'error'); return; } logToConsole('🚀 Iniciando extracción de contenido...', 'info'); updateProgress(10, 'Validando URL...'); try { const response = await fetch('/api/run-script', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ script: 'youtube_extractor.py', args: [url] }) }); updateProgress(30, 'Descargando audio...'); logToConsole('📥 Descargando audio del video...', 'info'); // Simular progreso mientras se procesa setTimeout(() => updateProgress(50, 'Transcribiendo con Whisper...'), 2000); setTimeout(() => updateProgress(70, 'Analizando contenido...'), 4000); setTimeout(() => updateProgress(90, 'Generando reporte...'), 6000); const result = await response.json(); if (response.ok) { updateProgress(100, '✅ Completado'); logToConsole('✅ Extracción completada exitosamente', 'success'); displayResults(result.data); } else { logToConsole(\`❌ Error: \${result.error}\`, 'error'); updateProgress(0, ''); } } catch (error) { logToConsole(\`❌ Error de conexión: \${error.message}\`, 'error'); updateProgress(0, ''); } } function displayResults(data) { const resultsContainer = document.getElementById('results-container'); const videoInfo = document.getElementById('video-info'); const transcription = document.getElementById('transcription'); const analysis = document.getElementById('analysis'); resultsContainer.classList.remove('hidden'); // Video Info videoInfo.innerHTML = \` <div class="bg-black/30 p-4 rounded-lg"> <h4 class="font-semibold text-yellow-400">📹 Título</h4> <p>\${data.title || 'No disponible'}</p> </div> <div class="bg-black/30 p-4 rounded-lg"> <h4 class="font-semibold text-yellow-400">⏱️ Duración</h4> <p>\${data.duration || 'No disponible'}</p> </div> <div class="bg-black/30 p-4 rounded-lg"> <h4 class="font-semibold text-yellow-400">👤 Canal</h4> <p>\${data.channel || 'No disponible'}</p> </div> <div class="bg-black/30 p-4 rounded-lg"> <h4 class="font-semibold text-yellow-400">📅 Fecha</h4> <p>\${data.upload_date || 'No disponible'}</p> </div> \`; // Transcription transcription.innerHTML = \` <div class="text-gray-300 leading-relaxed"> \${data.transcription || 'Transcripción no disponible'} </div> \`; // Analysis analysis.innerHTML = \` <div class="bg-black/30 p-4 rounded-lg"> <h4 class="font-semibold text-green-400">📊 Palabras</h4> <p class="text-2xl font-bold">\${data.word_count || 0}</p> </div> <div class="bg-black/30 p-4 rounded-lg"> <h4 class="font-semibold text-blue-400">😊 Sentimiento</h4> <p class="text-lg">\${data.sentiment || 'Neutral'}</p> </div> <div class="bg-black/30 p-4 rounded-lg"> <h4 class="font-semibold text-purple-400">🏷️ Tipo</h4> <p class="text-lg">\${data.content_type || 'General'}</p> </div> \`; } function clearResults() { document.getElementById('youtube-url').value = ''; document.getElementById('results-container').classList.add('hidden'); document.getElementById('console-output').innerHTML = '<div class="text-green-400">✅ YouTube Content Extractor listo...</div>'; logToConsole('🧹 Resultados limpiados', 'info'); } </script> </body> </html>`, 'scripts/youtube_extractor.py': `#!/usr/bin/env python3 """ YouTube Content Extractor con Whisper AI Extrae audio de YouTube y genera transcripciones usando OpenAI Whisper """ import os import sys import json import subprocess import tempfile import re from datetime import datetime from pathlib import Path def install_dependencies(): """Instala dependencias necesarias si no están disponibles""" dependencies = [ 'yt-dlp', 'openai-whisper', 'requests' ] for dep in dependencies: try: __import__(dep.replace('-', '_')) except ImportError: print(f"📦 Instalando {dep}...") subprocess.check_call([sys.executable, '-m', 'pip', 'install', dep]) def download_audio(url, output_dir): """Descarga el audio de un video de YouTube""" try: import yt_dlp ydl_opts = { 'format': 'bestaudio/best', 'outtmpl': os.path.join(output_dir, '%(title)s.%(ext)s'), 'postprocessors': [{ 'key': 'FFmpegExtractAudio', 'preferredcodec': 'wav', 'preferredquality': '192', }], 'quiet': True, 'no_warnings': True, } with yt_dlp.YoutubeDL(ydl_opts) as ydl: # Extraer información del video info = ydl.extract_info(url, download=False) # Descargar audio ydl.download([url]) # Encontrar el archivo de audio descargado title = info.get('title', 'unknown') safe_title = re.sub(r'[<>:"/\\|?*]', '_', title) audio_file = os.path.join(output_dir, f"{safe_title}.wav") # Buscar el archivo real (yt-dlp puede cambiar el nombre) for file in os.listdir(output_dir): if file.endswith('.wav'): audio_file = os.path.join(output_dir, file) break return audio_file, info except Exception as e: raise Exception(f"Error descargando audio: {str(e)}") def transcribe_audio(audio_file): """Transcribe audio usando Whisper""" try: import whisper print("🧠 Cargando modelo Whisper...") model = whisper.load_model("base") print("🎤 Transcribiendo audio...") result = model.transcribe(audio_file) return result["text"] except Exception as e: raise Exception(f"Error en transcripción: {str(e)}") def analyze_content(text): """Analiza el contenido transcrito""" if not text: return { "word_count": 0, "character_count": 0, "sentiment": "Neutral", "content_type": "Desconocido", "keywords": [] } # Análisis básico words = text.split() word_count = len(words) character_count = len(text) # Análisis de sentimiento básico positive_words = ['bueno', 'excelente', 'genial', 'increíble', 'fantástico', 'amor', 'feliz'] negative_words = ['malo', 'terrible', 'horrible', 'odio', 'triste', 'problema', 'error'] positive_count = sum(1 for word in words if word.lower() in positive_words) negative_count = sum(1 for word in words if word.lower() in negative_words) if positive_count > negative_count: sentiment = "Positivo" elif negative_count > positive_count: sentiment = "Negativo" else: sentiment = "Neutral" # Detección de tipo de contenido tutorial_keywords = ['tutorial', 'cómo', 'paso', 'aprende', 'enseña'] music_keywords = ['música', 'canción', 'ritmo', 'melodía'] news_keywords = ['noticia', 'actualidad', 'reportaje', 'información'] text_lower = text.lower() if any(keyword in text_lower for keyword in tutorial_keywords): content_type = "Tutorial" elif any(keyword in text_lower for keyword in music_keywords): content_type = "Música" elif any(keyword in text_lower for keyword in news_keywords): content_type = "Noticias" else: content_type = "General" # Palabras clave más frecuentes word_freq = {} for word in words: word = word.lower().strip('.,!?";') if len(word) > 3: # Solo palabras de más de 3 caracteres word_freq[word] = word_freq.get(word, 0) + 1 keywords = sorted(word_freq.items(), key=lambda x: x[1], reverse=True)[:10] keywords = [word for word, freq in keywords] return { "word_count": word_count, "character_count": character_count, "sentiment": sentiment, "content_type": content_type, "keywords": keywords } def save_results(data, output_dir): """Guarda los resultados en múltiples formatos""" timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") base_name = f"youtube_extraction_{timestamp}" # Guardar JSON json_file = os.path.join(output_dir, f"{base_name}.json") with open(json_file, 'w', encoding='utf-8') as f: json.dump(data, f, indent=2, ensure_ascii=False) # Guardar TXT txt_file = os.path.join(output_dir, f"{base_name}.txt") with open(txt_file, 'w', encoding='utf-8') as f: f.write(f"YouTube Content Extraction\\n") f.write(f"========================\\n\\n") f.write(f"Título: {data.get('title', 'N/A')}\\n") f.write(f"Canal: {data.get('channel', 'N/A')}\\n") f.write(f"Duración: {data.get('duration', 'N/A')}\\n") f.write(f"Fecha: {data.get('upload_date', 'N/A')}\\n\\n") f.write(f"TRANSCRIPCIÓN:\\n") f.write(f"=============\\n") f.write(data.get('transcription', 'No disponible')) f.write(f"\\n\\nANÁLISIS:\\n") f.write(f"=========\\n") analysis = data.get('analysis', {}) f.write(f"Palabras: {analysis.get('word_count', 0)}\\n") f.write(f"Sentimiento: {analysis.get('sentiment', 'N/A')}\\n") f.write(f"Tipo: {analysis.get('content_type', 'N/A')}\\n") # Guardar Markdown md_file = os.path.join(output_dir, f"{base_name}.md") with open(md_file, 'w', encoding='utf-8') as f: f.write(f"# YouTube Content Extraction\\n\\n") f.write(f"## 📹 Información del Video\\n\\n") f.write(f"- **Título:** {data.get('title', 'N/A')}\\n") f.write(f"- **Canal:** {data.get('channel', 'N/A')}\\n") f.write(f"- **Duración:** {data.get('duration', 'N/A')}\\n") f.write(f"- **Fecha:** {data.get('upload_date', 'N/A')}\\n\\n") f.write(f"## 📝 Transcripción\\n\\n") f.write(data.get('transcription', 'No disponible')) f.write(f"\\n\\n## 🧠 Análisis\\n\\n") analysis = data.get('analysis', {}) f.write(f"- **Palabras:** {analysis.get('word_count', 0)}\\n") f.write(f"- **Sentimiento:** {analysis.get('sentiment', 'N/A')}\\n") f.write(f"- **Tipo de contenido:** {analysis.get('content_type', 'N/A')}\\n") if analysis.get('keywords'): f.write(f"- **Palabras clave:** {', '.join(analysis['keywords'][:5])}\\n") return { "json_file": json_file, "txt_file": txt_file, "md_file": md_file } def main(): """Función principal""" try: # Verificar argumentos if len(sys.argv) < 2: raise Exception("URL de YouTube requerida como argumento") url = sys.argv[1] print("🚀 YouTube Content Extractor iniciado") print(f"🔗 URL: {url}") # Instalar dependencias si es necesario install_dependencies() # Crear directorio temporal with tempfile.TemporaryDirectory() as temp_dir: print(f"📁 Directorio temporal: {temp_dir}") # Descargar audio print("📥 Descargando audio...") audio_file, video_info = download_audio(url, temp_dir) print(f"✅ Audio descargado: {os.path.basename(audio_file)}") # Transcribir print("🎤 Iniciando transcripción...") transcription = transcribe_audio(audio_file) print("✅ Transcripción completada") # Analizar contenido print("🧠 Analizando contenido...") analysis = analyze_content(transcription) print("✅ Análisis completado") # Preparar datos result_data = { "url": url, "title": video_info.get('title'), "channel": video_info.get('uploader'), "duration": str(video_info.get('duration', 0)) + " segundos", "upload_date": video_info.get('upload_date'), "transcription": transcription, "analysis": analysis, "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S") } # Guardar resultados output_dir = "data" os.makedirs(output_dir, exist_ok=True) saved_files = save_results(result_data, output_dir) print("💾 Resultados guardados:") for file_type, file_path in saved_files.items(): print(f" - {file_type}: {file_path}") # Output para la API api_response = { "status": "success", "message": "Extracción completada exitosamente", "data": result_data, "files": saved_files } return json.dumps(api_response, indent=2, ensure_ascii=False) except Exception as e: error_response = { "status": "error", "message": f"Error en extracción: {str(e)}", "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S") } print(f"❌ Error: {str(e)}") return json.dumps(error_response, indent=2, ensure_ascii=False) if __name__ == "__main__": output = main() print("\\n" + "="*60) print("JSON OUTPUT:") print(output)`, 'tests/test_youtube.py': `#!/usr/bin/env python3 """ Tests para el YouTube Content Extractor """ import pytest import json import sys import os import tempfile # Agregar el directorio scripts al path sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'scripts')) def test_analyze_content(): """Test del análisis de contenido""" try: from youtube_extractor import analyze_content # Test con texto de ejemplo sample_text = "Este es un tutorial excelente sobre programación. Aprenderás paso a paso cómo crear aplicaciones increíbles." result = analyze_content(sample_text) # Verificar estructura assert "word_count" in result assert "sentiment" in result assert "content_type" in result assert "keywords" in result # Verificar valores assert result["word_count"] > 0 assert result["content_type"] == "Tutorial" assert result["sentiment"] == "Positivo" print("✅ Test analyze_content pasó correctamente") except Exception as e: pytest.fail(f"Test analyze_content falló: {str(e)}") def test_analyze_empty_content(): """Test del análisis con contenido vacío""" try: from youtube_extractor import analyze_content result = analyze_content("") assert result["word_count"] == 0 assert result["sentiment"] == "Neutral" assert result["content_type"] == "Desconocido" print("✅ Test analyze_empty_content pasó correctamente") except Exception as e: pytest.fail(f"Test analyze_empty_content falló: {str(e)}") def test_save_results(): """Test del guardado de resultados""" try: from youtube_extractor import save_results # Datos de prueba test_data = { "title": "Video de Prueba", "channel": "Canal Test", "transcription": "Esta es una transcripción de prueba", "analysis": { "word_count": 7, "sentiment": "Neutral", "content_type": "General" } } # Usar directorio temporal with tempfile.TemporaryDirectory() as temp_dir: saved_files = save_results(test_data, temp_dir) # Verificar que se crearon los archivos assert "json_file" in saved_files assert "txt_file" in saved_files assert "md_file" in saved_files # Verificar que los archivos existen for file_path in saved_files.values(): assert os.path.exists(file_path) # Verificar contenido JSON with open(saved_files["json_file"], 'r', encoding='utf-8') as f: loaded_data = json.load(f) assert loaded_data["title"] == test_data["title"] print("✅ Test save_results pasó correctamente") except Exception as e: pytest.fail(f"Test save_results falló: {str(e)}") def test_url_validation(): """Test de validación de URL""" valid_urls = [ "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "https://youtu.be/dQw4w9WgXcQ", "https://youtube.com/watch?v=dQw4w9WgXcQ" ] invalid_urls = [ "https://vimeo.com/123456", "https://example.com", "not_a_url" ] for url in valid_urls: assert "youtube" in url.lower() or "youtu.be" in url.lower() for url in invalid_urls: assert not ("youtube" in url.lower() or "youtu.be" in url.lower()) print("✅ Test url_validation pasó correctamente") if __name__ == "__main__": print("🧪 Ejecutando tests del YouTube Content Extractor...") test_analyze_content() test_analyze_empty_content() test_save_results() test_url_validation() print("\\n✅ Todos los tests pasaron correctamente!")`, 'scripts/install.sh': `#!/bin/bash # Script de instalación para YouTube Content Extractor # Evitar bucle infinito - si ya estamos instalando, salir if [ "$PERSONNNKIT_INSTALLING" = "true" ]; then echo "⚠️ Instalación ya en progreso, evitando bucle infinito..." exit 0 fi # Marcar que estamos instalando export PERSONNNKIT_INSTALLING=true echo "🚀 Instalando YouTube Content Extractor..." # Verificar Python if ! command -v python3 &> /dev/null; then echo "❌ Python3 no está instalado" exit 1 fi # Crear entorno virtual si no existe if [ ! -d "venv" ]; then echo "📦 Creando entorno virtual..." python3 -m venv venv fi # Activar entorno virtual echo "🔄 Activando entorno virtual..." source venv/bin/activate # Actualizar pip echo "⬆️ Actualizando pip..." pip install --upgrade pip # Instalar dependencias Python echo "📚 Instalando dependencias Python..." pip install fastapi uvicorn yt-dlp openai-whisper requests python-dotenv pytest # Verificar FFmpeg if ! command -v ffmpeg &> /dev/null; then echo "⚠️ FFmpeg no está instalado. Instalando..." if command -v brew &> /dev/null; then brew install ffmpeg elif command -v apt-get &> /dev/null; then sudo apt-get update && sudo apt-get install -y ffmpeg elif command -v yum &> /dev/null; then sudo yum install -y ffmpeg else echo "❌ No se pudo instalar FFmpeg automáticamente" echo "Por favor instala FFmpeg manualmente:" echo "- macOS: brew install ffmpeg" echo "- Ubuntu/Debian: sudo apt-get install ffmpeg" echo "- CentOS/RHEL: sudo yum install ffmpeg" exit 1 fi fi # Crear directorios necesarios echo "📁 Creando directorios..." mkdir -p data mkdir -p public/css # Hacer scripts ejecutables echo "🔧 Configurando permisos..." chmod +x scripts/*.py chmod +x scripts/*.sh # Limpiar variable de entorno unset PERSONNNKIT_INSTALLING echo "✅ Instalación completada!" echo "" echo "🎯 Para usar el extractor:" echo "1. Inicia el servidor: npm run dev" echo "2. Abre http://localhost:3333 en tu navegador"`, 'requirements.txt': `# YouTube Content Extractor Dependencies # Core Framework fastapi>=0.104.0 uvicorn>=0.24.0 # YouTube & Audio Processing yt-dlp>=2023.11.16 openai-whisper>=20231117 # Utilities requests>=2.31.0 python-dotenv>=1.0.0 # Testing pytest>=7.4.0 pytest-asyncio>=0.21.0 # Optional: Enhanced audio processing # torch>=2.0.0 # torchaudio>=2.0.0`, 'README.md': `# YouTube Content Extractor Agente especializado en extraer y analizar contenido de YouTube usando Whisper AI. ## 🎯 Características - ✅ Descarga audio de videos de YouTube - ✅ Transcripción automática con OpenAI Whisper - ✅ Análisis de sentimiento y contenido - ✅ Múltiples formatos de salida (JSON, TXT, MD) - ✅ Interfaz web moderna y responsive - ✅ Tests automatizados ## 🚀 Inicio Rápido \`\`\`bash # Instalar todas las dependencias (Node.js + Python + FFmpeg) npm install # Iniciar servidor de desarrollo npm run dev # Abrir en navegador open http://localhost:3333 \`\`\` ## 📋 Requisitos - Python 3.8+ - Node.js 16+ - FFmpeg (se instala automáticamente) ## 🎬 Uso 1. Pega una URL de YouTube en la interfaz web 2. Haz clic en "Extraer Contenido" 3. Espera a que se complete el procesamiento 4. Revisa los resultados y archivos generados ## 📁 Estructura \`\`\` youtube-extractor/ ├── pages/ # Interfaz web ├── scripts/ # Scripts de procesamiento ├── tests/ # Tests automatizados ├── data/ # Archivos generados └── venv/ # Entorno virtual Python \`\`\` ## 🧪 Testing \`\`\`bash # Ejecutar todos los tests npm run test # Test específico python -m pytest tests/test_youtube.py -v \`\`\` ## 📊 Formatos de Salida - **JSON**: Datos estructurados para APIs - **TXT**: Texto plano legible - **MD**: Markdown para documentación ## 🔧 Configuración Avanzada ### Variables de Entorno Crea un archivo \`.env\`: \`\`\` WHISPER_MODEL=base # tiny, base, small, medium, large OUTPUT_DIR=data MAX_DURATION=3600 # Máximo 1 hora \`\`\` ### Modelos Whisper Disponibles - \`tiny\`: Más rápido, menos preciso - \`base\`: Balance entre velocidad y precisión - \`small\`: Mejor precisión, más lento - \`medium\`: Alta precisión - \`large\`: Máxima precisión, muy lento ## 🤝 Contribuir 1. Fork el proyecto 2. Crea una rama para tu feature 3. Commit tus cambios 4. Push a la rama 5. Abre un Pull Request ## 📄 Licencia MIT License - ver archivo LICENSE para detalles. --- Construido con ❤️ usando **PersonnnKit**` } }; //# sourceMappingURL=youtube.js.map