tengo esto:

Folder Structure:

ia-web-scraping/
├─ static/
│   static/
│   ├─ js/
│   │   js/
│   │   └─ scripts.js
│   └─ css/
│       css/
│       └─ styles.css
├─ templates/
│   templates/
│   └─ index.html
├─ requirements.txt
├─ app.py
├─ twitter_scraper.py
└─ .gitignore


File Contents:

File: ia-web-scraping/static/js/scripts.js
==========================================
async function analyzeTopic() {
    const topic = document.getElementById("topic").value.trim();

    if (topic === "") {
        alert("Por favor, ingresa un tema.");
        return;
    }

    // Desactiva el botón y mostrar indicador de búsqueda
    const analyzeButton = document.getElementById("analyze-button");
    analyzeButton.disabled = true;
    const originalButtonText = analyzeButton.innerText;
    analyzeButton.innerText = "Buscando...";

    try {
        const response = await fetch("/analyze", {
            method: "POST",
            headers: {"Content-Type": "application/json"},
            body: JSON.stringify({topic})
        });

        if (!response.ok) {
            throw new Error("Error en la solicitud.");
        }

        const result = await response.json();
        renderResults(result);
    } catch (error) {
        console.error(error);
        alert("Ocurrió un error al procesar la solicitud.");
    } finally {
        // Reactiva el botón y restaurar el texto original
        analyzeButton.disabled = false;
        analyzeButton.innerText = originalButtonText;
    }
}

function renderResults(data) {
    const output = document.getElementById("results");
    output.innerHTML = "";

    if (data.tweets && data.tweets.length > 0) {
        // Crea sección para los tweets
        const tweetsSection = document.createElement("div");
        tweetsSection.classList.add("mb-4");

        const tweetsHeader = document.createElement("h2");
        tweetsHeader.innerText = "Tweets Obtenidos:";
        tweetsSection.appendChild(tweetsHeader);

        data.tweets.forEach((tweet, index) => {
            const tweetCard = document.createElement("div");
            tweetCard.classList.add("card", "tweet-card");

            const cardBody = document.createElement("div");
            cardBody.classList.add("card-body");

            const tweetTitle = document.createElement("h5");
            tweetTitle.classList.add("card-title");
            tweetTitle.innerText = `Tweet ${index + 1}:`;

            const tweetText = document.createElement("p");
            tweetText.classList.add("card-text");
            tweetText.innerText = tweet;

            cardBody.appendChild(tweetTitle);
            cardBody.appendChild(tweetText);
            tweetCard.appendChild(cardBody);
            tweetsSection.appendChild(tweetCard);
        });

        output.appendChild(tweetsSection);
    } else {
        const noTweets = document.createElement("p");
        noTweets.innerText = "No se encontraron tweets para el tema proporcionado.";
        output.appendChild(noTweets);
    }

    if (data.summary) {
        // Crea sección para el resumen
        const summarySection = document.createElement("div");
        summarySection.classList.add("summary-section");

        const summaryHeader = document.createElement("h2");
        summaryHeader.innerText = "Resumen:";
        summarySection.appendChild(summaryHeader);

        const summaryText = document.createElement("p");
        summaryText.innerText = data.summary;
        summarySection.appendChild(summaryText);

        output.appendChild(summarySection);
    }

    // Gráfica de Sentimiento
    if (data.sentiment_distribution) {
        const sentimentSection = document.createElement("div");
        sentimentSection.classList.add("mb-4");

        const sentimentHeader = document.createElement("h2");
        sentimentHeader.innerText = "Distribución de Sentimiento:";
        sentimentSection.appendChild(sentimentHeader);

        const sentimentCanvas = document.createElement("canvas");
        sentimentCanvas.id = "sentiment-chart";
        sentimentCanvas.width = 400;
        sentimentCanvas.height = 200;
        sentimentSection.appendChild(sentimentCanvas);

        output.appendChild(sentimentSection);

        new Chart(sentimentCanvas, {
            type: 'bar',
            data: {
                labels: Object.keys(data.sentiment_distribution),
                datasets: [{
                    data: Object.values(data.sentiment_distribution),
                    backgroundColor: ['#FF6384', '#36A2EB', '#FFCE56']
                }]
            },
            options: {
                responsive: true,
                plugins: {
                    legend: {
                        position: 'top',
                    },
                    title: {
                        display: true,
                        text: 'Distribución de Sentimiento'
                    }
                }
            }
        });
    }

    
    // Nube de Palabras
    if (data.wordcloud_img) {
        const wordcloudSection = document.createElement("div");
        wordcloudSection.classList.add("mb-4");

        const wordcloudHeader = document.createElement("h2");
        wordcloudHeader.innerText = "Nube de Palabras:";
        wordcloudSection.appendChild(wordcloudHeader);

        const wordcloudImg = document.createElement("img");
        wordcloudImg.src = `data:image/png;base64,${data.wordcloud_img}`;
        wordcloudImg.alt = "Nube de Palabras";
        wordcloudSection.appendChild(wordcloudImg);

        output.appendChild(wordcloudSection);
    }
}


File: ia-web-scraping/static/css/styles.css
===========================================
body {
    padding-top: 50px;
    background-color: #121212;
    color: #e0e0e0;
}

.container {
    max-width: 800px;
}

h1, h2, h5 {
    color: #ffffff;
}

.input-group .form-control {
    background-color: #1e1e1e;
    color: #e0e0e0;
    border: 1px solid #333333;
}

.input-group .form-control::placeholder {
    color: #888888;
}

.btn-primary {
    background-color: #1f6feb;
    border-color: #1f6feb;
}

.btn-primary:hover {
    background-color: #1558b0;
    border-color: #1558b0;
}

.form-check-label {
    color: #e0e0e0;
}

.card {
    background-color: #1e1e1e;
    border: 1px solid #333333;
}

.card-title {
    color: #ffffff;
}

.card-text {
    color: #cccccc;
}

.summary-section {
    margin-top: 30px;
}


File: ia-web-scraping/templates/index.html
==========================================
<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>Análisis de Tweets en X</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
    <link href="{{ url_for('static', filename='css/styles.css') }}" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/leaflet.css" />
</head>
<body>
    <div class="container">
        <h1 class="text-center mb-4">Análisis de Tweets en X</h1>
        <div class="input-group mb-3">
            <input type="text" id="topic" class="form-control" placeholder="Ingrese un tema" aria-label="Tema" aria-describedby="button-analyze">
            <button class="btn btn-primary" type="button" id="analyze-button" onclick="analyzeTopic()">Buscar</button>
        </div>
        <div id="results">
            <h2 class="mt-4">Resumen de Tweets</h2>
            <p id="summary"></p>

            <h2 class="mt-4">Distribución de Sentimiento</h2>
            <canvas id="sentiment-chart" width="400" height="200"></canvas>

            <h2 class="mt-4">Nube de Palabras</h2>
            <div id="wordcloud"></div>
        </div>

        <footer class="text-center mt-5">
            <p>Hecho por <a href="https://github.com/JohanMorales211" target="_blank">Johan Morales</a></p> 
        </footer>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <script src="https://d3js.org/d3.v7.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/leaflet.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.heat/0.2.0/leaflet-heat.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
    <script src="{{ url_for('static', filename='js/scripts.js') }}"></script>
</body>
</html>


File: ia-web-scraping/requirements.txt
======================================
Flask==3.1.0
python-dotenv==1.0.1
selenium==4.26.1
webdriver-manager==4.0.2
cerebras_cloud_sdk==1.11.0
textblob
matplotlib
wordcloud
pandas
folium


File: ia-web-scraping/app.py
============================
import os
import json
import time
import logging
import base64
from flask import Flask, request, jsonify, render_template, send_file
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.firefox.service import Service
from webdriver_manager.firefox import GeckoDriverManager
from selenium.webdriver.firefox.options import Options
from dotenv import load_dotenv
from cerebras.cloud.sdk import Cerebras
from textblob import TextBlob
from wordcloud import WordCloud
import matplotlib.pyplot as plt
import pandas as pd
import folium
from folium.plugins import HeatMap
from io import BytesIO
from datetime import datetime

# Configuración de logging
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

# Cargar variables del archivo .env
load_dotenv()

# Obtener las variables de entorno
CEREBRAS_API_KEY = os.getenv("CEREBRAS_API_KEY")
TWITTER_COOKIES_FILE = "cookies/twitter_cookies.json"

# Verifica si la API Key de Cerebras está cargada
if not CEREBRAS_API_KEY:
    logging.error("La variable de entorno CEREBRAS_API_KEY no está definida.")
    raise EnvironmentError("CEREBRAS_API_KEY no configurada en el archivo .env")

app = Flask(__name__)

def load_cookies(driver, filepath):
    """Carga las cookies almacenadas desde un archivo JSON en el navegador."""
    try:
        with open(filepath, "r") as f:
            cookies = json.load(f)
            for cookie in cookies:
                if 'domain' in cookie:
                    cookie['domain'] = '.x.com'
                driver.add_cookie(cookie)
        logging.debug(f"Cookies cargadas desde {filepath}")
    except Exception as e:
        logging.error(f"Error al cargar cookies desde {filepath}: {e}")

def init_driver_with_cookies():
    """Inicializa el navegador Firefox con las cookies cargadas para mantener la sesión."""
    try:
        options = Options()
        options.headless = False  # Siempre ejecutar con ventana visible
        logging.debug("Modo headless desactivado. Mostrando ventana del navegador.")

        driver = webdriver.Firefox(
            service=Service(GeckoDriverManager().install()),
            options=options
        )
        driver.get("https://x.com")
        load_cookies(driver, TWITTER_COOKIES_FILE)
        driver.refresh()
        logging.debug("Navegador inicializado y cookies cargadas.")
        return driver
    except Exception as e:
        logging.error(f"Error al inicializar el driver con cookies: {e}")
        raise

def get_tweets(driver, topic, desired_count=10, max_scrolls=10):
    """Scrapea los últimos tweets relacionados con el tópico proporcionado."""
    try:
        formatted_topic = topic.replace(' ', '+')
        search_url = f"https://x.com/search?q={formatted_topic}&src=typed_query"
        driver.get(search_url)
        logging.debug(f"Navegando a la URL de búsqueda: {search_url}")

        tweets = []
        scrolls = 0

        while len(tweets) < desired_count and scrolls < max_scrolls:
            time.sleep(3)  # Espera a que la página cargue los tweets
            tweet_elements = driver.find_elements(By.CSS_SELECTOR, 'article div[lang]')
            tweets = [element.text for element in tweet_elements]
            logging.debug(f"Tweets encontrados tras el scroll {scrolls + 1}: {len(tweets)}")

            if len(tweets) >= desired_count:
                break

            # Desplaza hacia abajo para cargar más tweets
            driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
            scrolls += 1

        # Retorna solo los primeros desired_count tweets
        tweets = tweets[:desired_count]
        logging.debug(f"Total de tweets obtenidos para el tema '{topic}': '{len(tweets)}'")
        return tweets
    except Exception as e:
        logging.error(f"Error al obtener tweets: {e}")
        return []

def analyze_tweets_summary_cerebras(tweets):
    if not tweets:
        logging.warning("No se proporcionaron tweets para analizar.")
        return "No se encontraron tweets para resumir."

    # Unir todos los tweets en un solo texto
    combined_text = "\n".join(tweets)
    logging.debug("Textos de tweets combinados para el resumen.")

    # Inicializar el cliente de Cerebras
    client = Cerebras(api_key=CEREBRAS_API_KEY)

    try:
        # Crea la solicitud de completions con un prompt orientado a generar un resumen cohesivo
        response = client.chat.completions.create(
            messages=[
                {
                    "role": "system",
                    "content": "Eres un asistente que resume textos de Tweets en un tono neutral, generando un resumen cohesivo en párrafos que describa el contexto general."
                },
                {
                    "role": "user",
                    "content": f"Resume los siguientes tweets de manera clara y neutral, generando varios párrafos que describan el contexto general:\n{combined_text}"
                }
            ],
            model="llama3.1-8b",
            stream=False,
            max_completion_tokens=5000,
            temperature=0.2,
            top_p=1
        )

        logging.debug(f"Respuesta de Cerebras: {response}")

        if isinstance(response, tuple):
            logging.debug("La respuesta es una tupla.")
            response = response[0]

        if hasattr(response, 'choices') and len(response.choices) > 0:
            choice = response.choices[0]
            if hasattr(choice, 'message') and hasattr(choice.message, 'content'):
                summary = choice.message.content
                logging.debug(f"Resumen generado: {summary}")
                return summary.strip()
            else:
                logging.error("El primer elemento de 'choices' no tiene 'message.content'.")
                return "Error al generar el resumen."
        else:
            logging.error("La respuesta de la API no contiene 'choices' o está vacía.")
            return "Error al generar el resumen."

    except Exception as e:
        logging.error(f"Error al generar el resumen con Cerebras: {e}")
        return "Error al generar el resumen."

def analyze_sentiment(tweets):
    """Analiza el sentimiento de los tweets y devuelve la distribución."""
    sentiments = []
    for tweet in tweets:
        analysis = TextBlob(tweet)
        sentiment = analysis.sentiment.polarity
        if sentiment > 0:
            sentiments.append('positivo')
        elif sentiment < 0:
            sentiments.append('negativo')
        else:
            sentiments.append('neutral')
    return sentiments

def generate_wordcloud(tweets):
    """Genera una nube de palabras a partir de los tweets."""
    wordcloud = WordCloud(width=800, height=400, background_color='white').generate(' '.join(tweets))
    plt.figure(figsize=(10, 5))
    plt.imshow(wordcloud, interpolation='bilinear')
    plt.axis('off')
    img = BytesIO()
    plt.savefig(img, format='png')
    plt.close()
    img.seek(0)
    return base64.b64encode(img.getvalue()).decode('utf-8')

@app.route("/", methods=["GET"])
def index():
    """Ruta principal que renderiza el template index.html."""
    logging.debug("Cargando página principal.")
    return render_template("index.html")

@app.route("/analyze", methods=["POST"])
def analyze():
    """Ruta que recibe un tópico, obtiene tweets relacionados y genera un resumen."""
    try:
        data = request.get_json()
        if not data:
            logging.warning("Solicitud POST sin cuerpo JSON.")
            return jsonify({"summary": "Solicitud inválida."}), 400

        topic = data.get("topic")

        logging.debug(f"Tópico recibido para análisis: {topic}")

        if not topic:
            logging.warning("No se proporcionó un tópico en la solicitud.")
            return jsonify({"summary": "No se proporcionó un tópico para analizar."}), 400

        driver = init_driver_with_cookies()
        tweets = get_tweets(driver, topic)
        driver.quit()
        logging.debug("Driver cerrado después de obtener tweets.")

        # Genera un resumen de todos los tweets usando Cerebras
        summary = analyze_tweets_summary_cerebras(tweets)
        logging.debug(f"Resumen generado para el tópico '{topic}': {summary}")

        # Analiza el sentimiento de los tweets
        sentiments = analyze_sentiment(tweets)
        sentiment_distribution = {
            'positivo': sentiments.count('positivo'),
            'negativo': sentiments.count('negativo'),
            'neutral': sentiments.count('neutral')
        }

        # Genera una nube de palabras
        wordcloud_img = generate_wordcloud(tweets)

        # Devuelve los tweets, resumen, distribuciones y las imágenes en base64
        return jsonify({
            "tweets": tweets,
            "summary": summary,
            "sentiment_distribution": sentiment_distribution,
            "wordcloud_img": wordcloud_img,
            "frequency_img": frequency_img,
            "heatmap_data": heatmap_data  # Envia datos de mapa de calor
        })

    except Exception as e:
        logging.error(f"Error en la ruta /analyze: {e}")
        return jsonify({"summary": "Error interno del servidor."}), 500  # Código HTTP corregido

if __name__ == "__main__":
    # Ejecuta la aplicación Flask
    app.run(debug=True)


File: ia-web-scraping/twitter_scraper.py
========================================
import os
import time
import json
from dotenv import load_dotenv
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.firefox.service import Service
from webdriver_manager.firefox import GeckoDriverManager

# Carga las variables del archivo .env
load_dotenv()
X_USERNAME = os.getenv("X_USERNAME")
X_PASSWORD = os.getenv("X_PASSWORD")

COOKIES_FOLDER = "cookies"
os.makedirs(COOKIES_FOLDER, exist_ok=True)
COOKIES_FILE = os.path.join(COOKIES_FOLDER, "twitter_cookies.json")

def init_driver():
    driver = webdriver.Firefox(service=Service(GeckoDriverManager().install()))
    return driver

def save_cookies(driver, filepath):
    with open(filepath, "w") as f:
        json.dump(driver.get_cookies(), f)

def login_to_twitter(driver):
    driver.get("https://x.com/i/flow/login")
    time.sleep(5)
    driver.find_element(By.NAME, "text").send_keys(X_USERNAME + Keys.RETURN)
    time.sleep(3)
    driver.find_element(By.NAME, "password").send_keys(X_PASSWORD + Keys.RETURN)
    time.sleep(5)

if __name__ == "__main__":
    driver = init_driver()
    login_to_twitter(driver)
    save_cookies(driver, COOKIES_FILE)
    driver.quit()
    print("[INFO] Cookies guardadas exitosamente.")



File: ia-web-scraping/.gitignore
================================
Contenido binario o no legible.


pero neceisot que me coloques en los requeriments los que estoy utilizando en mi proyecto, los que no, los eliminas