mira, tengo esto:

import os
import json
import time
import logging
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 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:
                # Asegura de que el dominio de la cookie coincide con el dominio actual
                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.
    Siempre muestra la ventana del navegador (no headless).
    """
    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 hasta alcanzar desired_count.
    :param driver: Instancia del navegador Selenium.
    :param topic: Tópico para buscar tweets.
    :param desired_count: Número de tweets deseados.
    :param max_scrolls: Número máximo de desplazamientos para intentar.
    :return: Lista de tweets.
    """
    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]')
            current_count = len(tweet_elements)
            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
        )

        # Registra la respuesta para inspección
        logging.debug(f"Respuesta de Cerebras: {response}")

        # Registra el tipo de respuesta
        logging.debug(f"Tipo de respuesta de Cerebras: {type(response)}")

        # Maneja la respuesta dependiendo de su estructura
        if isinstance(response, tuple):
            logging.debug("La respuesta es una tupla.")
            response = response[0]

        if hasattr(response, 'choices') and len(response.choices) > 0:
            # Accede a la primera elección
            choice = response.choices[0]

            # Accede al contenido del mensaje
            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')
    img.seek(0)
    return img

def generate_tweet_frequency(tweets):
    """
    Genera una gráfica de frecuencia de tweets a lo largo del tiempo.
    """
    # Simulamos fechas de tweets
    dates = [datetime.now() - pd.Timedelta(minutes=i) for i in range(len(tweets))]
    df = pd.DataFrame({'date': dates})
    df['date'] = pd.to_datetime(df['date']).dt.date
    freq = df['date'].value_counts().sort_index()
    plt.figure(figsize=(10, 5))
    plt.plot(freq.index, freq.values, marker='o')
    plt.xlabel('Fecha')
    plt.ylabel('Frecuencia de Tweets')
    plt.title('Frecuencia de Tweets a lo largo del Tiempo')
    plt.xticks(rotation=45)
    plt.tight_layout()
    img = BytesIO()
    plt.savefig(img, format='png')
    img.seek(0)
    return img

def generate_heatmap(tweets):
    """
    Genera un mapa de calor geográfico a partir de los tweets.
    """
    # Simulamos ubicaciones de tweets
    data = (
        pd.DataFrame({
            'lat': [40.4168, 40.4168, 41.3851, 41.3851],
            'lon': [-3.7038, -3.7038, 2.1734, 2.1734],
            'weight': [1, 1, 1, 1]
        })
    )
    m = folium.Map(location=[40.4168, -3.7038], zoom_start=5)
    heat_data = [[row['lat'], row['lon'], row['weight']] for index, row in data.iterrows()]
    folium.plugins.HeatMap(heat_data).add_to(m)
    img_data = m._to_png(5)
    return img_data

@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.
    Siempre muestra la ventana del navegador durante el scraping.
    """
    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)

        # Genera una gráfica de frecuencia de tweets a lo largo del tiempo
        frequency_img = generate_tweet_frequency(tweets)

        # Genera un mapa de calor geográfico
        heatmap_img = generate_heatmap(tweets)

        # Devuelve tanto los tweets como el resumen
        return jsonify({
            "tweets": tweets,
            "summary": summary,
            "sentiment_distribution": sentiment_distribution,
            "wordcloud_img": wordcloud_img.getvalue().decode('utf-8'),
            "frequency_img": frequency_img.getvalue().decode('utf-8'),
            "heatmap_img": heatmap_img.decode('utf-8')
        })

    except Exception as e:
        logging.error(f"Error en la ruta /analyze: {e}")
        return jsonify({"summary": "Error interno del servidor."}), 50
    
if __name__ == "__main__":
    # Ejecuta la aplicación Flask
    app.run(debug=True)


este es mi 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}`;
        wordcloudSection.appendChild(wordcloudImg);

        output.appendChild(wordcloudSection);
    }

    // Gráfica de Frecuencia de Tweets a lo largo del Tiempo
    if (data.frequency_img) {
        const frequencySection = document.createElement("div");
        frequencySection.classList.add("mb-4");

        const frequencyHeader = document.createElement("h2");
        frequencyHeader.innerText = "Frecuencia de Tweets a lo largo del Tiempo:";
        frequencySection.appendChild(frequencyHeader);

        const frequencyCanvas = document.createElement("canvas");
        frequencyCanvas.id = "frequency-chart";
        frequencyCanvas.width = 400;
        frequencyCanvas.height = 200;
        frequencySection.appendChild(frequencyCanvas);

        output.appendChild(frequencySection);

        new Chart(frequencyCanvas, {
            type: 'line',
            data: {
                labels: [],
                datasets: [{
                    label: 'Frecuencia de Tweets',
                    data: [],
                    fill: false,
                    borderColor: 'rgb(75, 192, 192)',
                    tension: 0.1
                }]
            },
            options: {
                responsive: true,
                plugins: {
                    legend: {
                        position: 'top',
                    },
                    title: {
                        display: true,
                        text: 'Frecuencia de Tweets a lo largo del Tiempo'
                    }
                },
                scales: {
                    x: {
                        type: 'time',
                        time: {
                            unit: 'day'
                        },
                        title: {
                            display: true,
                            text: 'Fecha'
                        }
                    },
                    y: {
                        title: {
                            display: true,
                            text: 'Frecuencia de Tweets'
                        }
                    }
                }
            }
        });
    }

    // Mapa de Calor Geográfico
    if (data.heatmap_img) {
        const heatmapSection = document.createElement("div");
        heatmapSection.classList.add("mb-4");

        const heatmapHeader = document.createElement("h2");
        heatmapHeader.innerText = "Mapa de Calor Geográfico:";
        heatmapSection.appendChild(heatmapHeader);

        const heatmapDiv = document.createElement("div");
        heatmapDiv.id = "heatmap";
        heatmapDiv.style.height = "400px";
        heatmapSection.appendChild(heatmapDiv);

        output.appendChild(heatmapSection);

        const heatmapData = `data:image/png;base64,${data.heatmap_img}`;
        const heatmap = L.map('heatmap').setView([40.4168, -3.7038], 5);
        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            maxZoom: 19,
            attribution: '© OpenStreetMap'
        }).addTo(heatmap);
        const heatmapImage = L.imageOverlay(heatmapData, [[30, -20], [50, 40]]).addTo(heatmap);
    }
}

y este es mi 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">
    <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/wordcloud2.js/1.0.6/WordCloud2.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/leaflet.js"></script>
    <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>

            <h2 class="mt-4">Frecuencia de Tweets a lo largo del Tiempo</h2>
            <canvas id="frequency-chart" width="400" height="200"></canvas>

            <h2 class="mt-4">Mapa de Calor Geográfico</h2>
            <div id="heatmap" style="height: 400px;"></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/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
    <script src="{{ url_for('static', filename='js/scripts.js') }}"></script>
</body>
</html>

pero me sale  error en el html diciendo: Ocurrió un error al procesar la solicitud.