@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
JavaScript
"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