dd
Hai detto:
https://github.com/gitmylo/audio-webui ti va di aiutarmi e dirmi come mettere questo nel mio codice e togliere tutto quello che tiene elevenlabs dal mio codice import os
import logging
import requests
import io
import webbrowser
import psutil
import threading
import queue
import speech_recognition as sr
from pydub import AudioSegment
from pydub.playback import play
import datetime
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from time import sleep

# Flask e Flask-SocketIO per la GUI + animazione in tempo reale
from flask import Flask
from flask_socketio import SocketIO

try:
    import cv2
    HAS_OPENCV = True
except ImportError:
    HAS_OPENCV = False

try:
    from googletrans import Translator
    HAS_TRANSLATOR = True
except ImportError:
    HAS_TRANSLATOR = False

# Importa 11Labs Unleashed
from elevenlabs_unleashed.tts import UnleashedTTS

# -----------------------------
# CONFIGURAZIONI BASE
# -----------------------------
logging.basicConfig(level=logging.INFO)
ASSISTANT_NAME = "Gideon"

# Parole chiave di attivazione
ACTIVATION_COMMANDS = ["gideon", "gideo", "gideoun", "gigget", "g", "gide", "giddy"]

# Chiavi API (inserisci le tue)
API_KEYS = {
  "news": "",  # Sostituisci con la tua chiave API per le notizie
    "weather": "",  # Sostituisci con la tua chiave API per il meteo
    "elevenlabs": "",  # **RIMUOVI LA CHIAVE API QUI** "11Labs Unleashed" si occupa di creare account e chiavi
    "gemini": "",     # <-- Sostituisci con la tua chiave API Gemini
}

# Variabili globali
promemoria_list = []
todo_list = []
conversation_memory = []
IS_SPEAKING = False

# Prompt di personalità (il "carattere" di Gideon)
persona_prompt = (
    "Sei Gideon, un assistente virtuale dotato di una personalità curiosa e amichevole. "
    "Cerchi di imparare e memorizzare ciò che dice l'utente, e rispondi in modo utile. "
    "Hai un pizzico di umorismo e cerchi di mostrare empatia nelle risposte. "
    "Ti interessano la scienza, la tecnologia, le culture e le lingue."
)

# Flask app + SocketIO
app = Flask(__name__)
app.config['SECRET_KEY'] = 'qualcosa_di_segretissimo'
socketio = SocketIO(app, cors_allowed_origins='*')

# -----------------------------
# THREAD E CODA PER IL TTS
# -----------------------------
# Rimuovi VOICE_ID, 11Labs Unleashed gestisce la voce
# VOICE_ID = None
speech_queue = queue.Queue()

# Inizializza UnleashedTTS
tts = UnleashedTTS(nb_accounts=2, create_accounts_threads=2)


# Rimuovi get_voice_id (11Labs Unleashed lo gestisce autonomamente)
# def get_voice_id(voice_name="Bella"):
#     """
#     Recupera l'ID della voce dall'API di ElevenLabs in base al nome scelto.
#     Se non trova la voce esatta, restituisce la prima disponibile.
#     """
#     if not API_KEYS["elevenlabs"]:
#         logging.warning("[WARNING] Chiave ElevenLabs non impostata. TTS non funzionante.")
#         return None
#     headers = {"xi-api-key": API_KEYS["elevenlabs"]}
#     try:
#         response = requests.get("https://api.elevenlabs.io/v1/voices", headers=headers)
#         if response.status_code == 200:
#             voices = response.json().get("voices", [])
#             # Stampa tutte le voci disponibili
#             for v in voices:
#                 print(f"Voice Name: {v['name']}, Voice ID: {v['voice_id']}")
#             # Cerca la voce desiderata
#             for v in voices:
#                 if v["name"].lower() == voice_name.lower():
#                     return v["voice_id"]
#             if voices:
#                 return voices[0]["voice_id"]
#         else:
#             logging.error(f"[ERRORE] Recupero voci fallito: {response.text}")
#     except Exception as e:
#         logging.error(f"[ERRORE] get_voice_id: {e}")
#     return None

def speak_worker():
    """
    Thread che gestisce la coda dei testi da pronunciare uno alla volta.
    """
    while True:
        text = speech_queue.get()
        if text is None:
            break
        _speak_internal(text)
        speech_queue.task_done()

def speak(text):
    """
    Inserisce il testo in coda per la sintesi vocale.
    """
    if text:
        speech_queue.put(text)
        # Invia il testo da visualizzare sul front-end
        socketio.emit('assistant_response', {'text': text})

def _speak_internal(text):
    """
    Chiamata effettiva alle API di ElevenLabs, spezzando il testo in chunk se troppo lungo.
    Imposta parametri di TTS più 'calmi' e 'dolci'.
    """
    global IS_SPEAKING
    IS_SPEAKING = True

    # Notifichiamo la GUI che stiamo iniziando a parlare
    socketio.emit('speaking_status', {'speaking': True})


    # Suddividi testo in chunk se > 500 caratteri
    MAX_CHARS = 500
    chunks = []
    if len(text) <= MAX_CHARS:
        chunks.append(text)
    else:
        start = 0
        while start < len(text):
            end = min(start + MAX_CHARS, len(text))
            chunks.append(text[start:end])
            start = end

    # Parametri TTS: voce più calma e rilassante (questi non sono passati all'API tramite UnleashedTTS)
    # voice_settings = {
    #     "stability": 0.85,          # Voce stabile
    #     "similarity_boost": 0.9     # Alta somiglianza con la voce campione
    # }

    for chunk in chunks:
        try:
            # usa UnleashedTTS per la sintesi vocale
           audio = tts.speak(chunk, voice="Bella", model="eleven_multilingual_v1") # voce femminile rilassante
           if audio:
             audio_segment = AudioSegment.from_file(io.BytesIO(audio), format="mp3")
             play(audio_segment)
        except Exception as e:
            logging.error(f"[ERRORE] TTS: {e}")
    IS_SPEAKING = False
    # Fine della sintesi: ricomponi la sfera
    socketio.emit('speaking_status', {'speaking': False})

# -----------------------------
# INPUT VOCALE
# -----------------------------
def take_command():
    """
    Ascolta dal microfono e ritorna la trascrizione in italiano (stringa minuscola).
    Ritorna "" in caso di errori / timeout.
    """
    global IS_SPEAKING
    r = sr.Recognizer()

    # Non ascoltare mentre stiamo parlando
    while IS_SPEAKING:
        sleep(0.1)

    with sr.Microphone() as source:
        r.adjust_for_ambient_noise(source, duration=0.5)
        try:
            audio = r.listen(source, timeout=5, phrase_time_limit=5)
            command = r.recognize_google(audio, language='it-IT').lower()
            # Invia il testo riconosciuto al front-end
            socketio.emit('user_speech', {'text': command})
            return command
        except sr.WaitTimeoutError:
            return ""
        except sr.UnknownValueError:
            return ""
        except Exception as e:
            logging.error(f"[ERRORE] take_command: {e}")
            return ""

# -----------------------------
# UTILITY PER IL LINGUAGGIO NATURALE
# -----------------------------
def is_activation_command(command):
    """Controlla se la frase contiene una keyword di attivazione."""
    return any(cmd in command for cmd in ACTIVATION_COMMANDS)

def maybe_personal_reply(user_input):
    """Risposte semplici a domande come 'come stai?', 'grazie', ecc."""
    if "come stai" in user_input:
        speak("Sto bene, grazie! Mi fa piacere che tu me l'abbia chiesto.")
        return True
    if "chi sei" in user_input or "presentati" in user_input:
        speak(f"Sono {ASSISTANT_NAME}, un assistente virtuale curioso e amichevole. "
              "Cerco di imparare da ogni conversazione.")
        return True
    if "grazie" in user_input:
        speak("Di nulla, è un piacere aiutarti.")
        return True
    return False

# -----------------------------
# FUNZIONI DI UTILITÀ CLASSICHE (meteo, news, etc.)
# -----------------------------
def control_smart_device(sub_cmd):
    """Esempio di controllo di un ipotetico dispositivo smart."""
    if "accendi luce" in sub_cmd:
        speak("Ok, luce accesa.")
        return True
    elif "spegni luce" in sub_cmd:
        speak("Ok, luce spenta.")
        return True
    return False

def weather():
    if not API_KEYS["weather"]:
        speak("Mi dispiace, non ho la chiave meteo configurata.")
        return
    speak("Dimmi la città per cui vuoi sapere il meteo.")
    city = take_command()
    if not city:
        return
    try:
        url = "http://api.openweathermap.org/data/2.5/weather"
        params = {
            "q": city,
            "appid": API_KEYS["weather"],
            "units": "metric",
            "lang": "it"
        }
        res = requests.get(url, params=params).json()
        if res.get("cod") == 200:
            descrizione = res['weather'][0]['description']
            temperatura = res['main']['temp']
            speak(f"A {city} ci sono {temperatura} gradi e il cielo è {descrizione}.")
        else:
            speak("Non ho trovato questa città, mi dispiace.")
    except Exception as e:
        logging.error(f"[ERRORE] weather: {e}")
        speak("Errore nel recupero del meteo.")

def fetch_news():
    if not API_KEYS["news"]:
        speak("Mi dispiace, non ho la chiave per le notizie configurata.")
        return
    speak("Recupero le notizie più importanti...")
    try:
        url = "https://newsapi.org/v2/top-headlines"
        params = {"country": "it", "apiKey": API_KEYS["news"]}
        response = requests.get(url, params=params)
        if response.status_code == 200:
            articles = response.json().get("articles", [])
            if articles:
                speak("Ecco alcune notizie di oggi:")
                for article in articles[:5]:
                    speak(article.get("title", ""))
            else:
                speak("Non ho trovato notizie rilevanti.")
        else:
            logging.error(f"[ERRORE] fetch_news: {response.text}")
            speak("Errore nel recupero delle notizie.")
    except Exception as e:
        logging.error(f"[ERRORE] fetch_news: {e}")
        speak("Errore nella richiesta delle notizie.")

def monitor_resources():
    try:
        cpu_usage = psutil.cpu_percent(interval=0.3)
        memory_usage = psutil.virtual_memory().percent
        speak(f"Utilizzo CPU: {cpu_usage}%. Memoria: {memory_usage}%.")
    except Exception as e:
        logging.error(f"[ERRORE] monitor_resources: {e}")
        speak("Errore nel monitoraggio delle risorse.")

def open_webpage():
    speak("Quale sito vuoi aprire?")
    url = take_command()
    if not url:
        return
    if not (url.startswith("http://") or url.startswith("https://")):
        url = "http://" + url
    try:
        webbrowser.open(url)
        speak("Sito aperto.")
    except Exception as e:
        logging.error(f"[ERRORE] open_webpage: {e}")
        speak("Non riesco ad aprire il sito.")

def show_calendar():
    events = [
        "Riunione col team alle 10.",
        "Pranzo con un'amica alle 13.",
        "Allenamento alle 18:30."
    ]
    if not events:
        speak("Al momento il tuo calendario è vuoto.")
    else:
        speak("Il tuo calendario prevede:")
        for e in events:
            speak(e)

def unit_converter():
    speak("Dimmi: 'chilometri in miglia' o 'miglia in chilometri'.")
    conversion_type = take_command()
    if not conversion_type:
        return
    if "chilometri" in conversion_type and "miglia" in conversion_type:
        speak("Quanti chilometri vuoi convertire?")
        number_str = take_command()
        try:
            km = float(number_str.replace(",", "."))
            miles = km * 0.621371
            speak(f"{km} chilometri sono circa {round(miles,2)} miglia.")
        except:
            speak("Numero non chiaro.")
    elif "miglia" in conversion_type and "chilometri" in conversion_type:
        speak("Quante miglia vuoi convertire?")
        number_str = take_command()
        try:
            miles = float(number_str.replace(",", "."))
            km = miles / 0.621371
            speak(f"{miles} miglia sono circa {round(km,2)} chilometri.")
        except:
            speak("Numero non chiaro.")
    else:
        speak("Conversione non chiara.")

def set_reminder():
    global promemoria_list
    speak("Vuoi aggiungere un promemoria o sentire quelli esistenti?")
    choice = take_command()
    if not choice:
        return
    if "aggiungi" in choice:
        speak("Qual è il testo del promemoria?")
        reminder_text = take_command()
        if reminder_text:
            promemoria_list.append(reminder_text)
            speak("Promemoria aggiunto.")
    elif "vedi" in choice or "leggi" in choice:
        if not promemoria_list:
            speak("Non hai promemoria salvati.")
        else:
            speak("Ecco i tuoi promemoria:")
            for i, rem in enumerate(promemoria_list, 1):
                speak(f"{i}. {rem}")
    else:
        speak("Scelta non riconosciuta.")

def tell_joke():
    jokes = [
        "Perché il pollo attraversa la strada? Per passare dall'altra parte!",
        "Che tempo fa su Google? C'è una ricerca in corso!"
    ]
    speak(jokes[0])

def take_photo():
    if not HAS_OPENCV:
        speak("Non posso scattare foto senza OpenCV.")
        return
    speak("Scatto in corso, sorridi!")
    cap = cv2.VideoCapture(0)
    if not cap.isOpened():
        speak("Nessuna fotocamera trovata.")
        return
    ret, frame = cap.read()
    if ret:
        filename = "foto_gideon.jpg"
        cv2.imwrite(filename, frame)
        speak("Foto salvata come foto_gideon.jpg.")
    else:
        speak("Errore nello scatto.")
    cap.release()
    cv2.destroyAllWindows()

def search_google():
    speak("Cosa vuoi cercare su Google?")
    query = take_command()
    if not query:
        return
    url = "https://www.google.com/search?q=" + query.replace(" ", "+")
    webbrowser.open(url)
    speak(f"Ricerca di {query} su Google.")

def tell_time():
    now = datetime.datetime.now()
    speak(f"Sono le {now.hour}:{now.minute}.")

def calculator():
    speak("Dimmi l'operazione da eseguire, per esempio '2 più 3'.")
    operation = take_command()
    if not operation:
        return
    try:
        tokens = operation.split()
        if "più" in tokens:
            nums = [float(x.replace(",", ".")) for x in tokens if x.replace(",", ".").isdigit()]
            res = sum(nums)
            speak(f"Il risultato è {res}")
        elif "meno" in tokens:
            nums = [float(x.replace(",", ".")) for x in tokens if x.replace(",", ".").isdigit()]
            if len(nums) == 2:
                speak(f"Il risultato è {nums[0] - nums[1]}")
            else:
                speak("Formato non chiaro.")
        elif "per" in tokens:
            nums = [float(x.replace(",", ".")) for x in tokens if x.replace(",", ".").isdigit()]
            res = 1
            for n in nums:
                res *= n
            speak(f"Il risultato è {res}")
        elif "diviso" in tokens:
            nums = [float(x.replace(",", ".")) for x in tokens if x.replace(",", ".").isdigit()]
            if len(nums) == 2 and nums[1] != 0:
                speak(f"Il risultato è {nums[0] / nums[1]}")
            else:
                speak("Non posso dividere per zero o formato sbagliato.")
        else:
            speak("Operazione non compresa.")
    except:
        speak("Errore nel calcolo.")

def manage_todo():
    global todo_list
    speak("Vuoi aggiungere, rimuovere o leggere la lista di cose da fare?")
    choice = take_command()
    if not choice:
        return
    if "aggiungi" in choice:
        speak("Dimmi la voce da aggiungere.")
        task = take_command()
        if task:
            todo_list.append(task)
            speak("Aggiunto.")
    elif "rimuovi" in choice:
        if not todo_list:
            speak("La lista è vuota.")
            return
        speak("Dimmi il numero dell'elemento da rimuovere, es. '1' per il primo.")
        index_str = take_command()
        if index_str.isdigit():
            idx = int(index_str) - 1
            if 0 <= idx < len(todo_list):
                removed = todo_list.pop(idx)
                speak(f"Ho rimosso {removed}.")
            else:
                speak("Indice non valido.")
        else:
            speak("Non ho capito.")
    elif "leggi" in choice or "mostra" in choice:
        if not todo_list:
            speak("La lista è vuota.")
        else:
            speak("Ecco le tue cose da fare:")
            for i, item in enumerate(todo_list, 1):
                speak(f"{i}. {item}")
    else:
        speak("Comando non riconosciuto.")

def send_email():
    speak("A chi vuoi inviare l'email?")
    recipient = take_command()
    if not recipient:
        return
    speak("Oggetto dell'email?")
    subject = take_command()
    if not subject:
        return
    speak("Testo del messaggio?")
    body = take_command()
    if not body:
        return

    # Esempio con Gmail (assicurati di avere le credenziali corrette / App Password)
    sender_email = "tuo.indirizzo@gmail.com"
    sender_password = "la_tua_password_o_app_password"
    receiver_email = recipient

    try:
        msg = MIMEMultipart()
        msg["From"] = sender_email
        msg["To"] = receiver_email
        msg["Subject"] = subject
        msg.attach(MIMEText(body, "plain"))

        server = smtplib.SMTP("smtp.gmail.com", 587)
        server.starttls()
        server.login(sender_email, sender_password)
        server.send_message(msg)
        server.quit()
        speak("Email inviata correttamente.")
    except Exception as e:
        logging.error(f"[ERRORE] send_email: {e}")
        speak("Errore durante l'invio dell'email.")

def translate_text():
    if not HAS_TRANSLATOR:
        speak("Manca il modulo googletrans, non posso tradurre.")
        return
    speak("Dimmi la frase da tradurre.")
    phrase = take_command()
    if not phrase:
        return
    speak("Vuoi tradurla in inglese o in italiano?")
    lang = take_command()
    if not lang:
        return

    translator = Translator()
    try:
        if "inglese" in lang:
            result = translator.translate(phrase, src='it', dest='en')
            speak(f"La traduzione in inglese è: {result.text}")
        elif "italiano" in lang:
            result = translator.translate(phrase, src='en', dest='it')
            speak(f"La traduzione in italiano è: {result.text}")
        else:
            speak("Lingua non riconosciuta.")
    except Exception as e:
        logging.error(f"[ERRORE] translate_text: {e}")
        speak("Errore durante la traduzione.")

# -----------------------------
# FUNZIONE PER ELENCARE I MODELLI DISPONIBILI
# -----------------------------
def list_available_models():
    """
    Elenca i modelli disponibili con la chiave API fornita.
    """
    if not API_KEYS["gemini"]:
        logging.error("Chiave API di Gemini non impostata.")
        return

    API_KEY = API_KEYS["gemini"]
    url = f"https://generativelanguage.googleapis.com/v1beta3/models?key={API_KEY}"
    headers = {
        "Content-Type": "application/json"
    }
    try:
        response = requests.get(url, headers=headers)
        if response.status_code == 200:
            models = response.json()
            print("Modelli disponibili:")
            for model in models.get("models", []):
                print(model["name"])
        else:
            print(f"Errore nel recupero dei modelli: {response.status_code}, {response.text}")
    except Exception as e:
        print(f"Eccezione durante il recupero dei modelli: {e}")

# -----------------------------
# CHIAMATA A Gemini con chiave API
# -----------------------------
def gemini_query(question):
    """
    Esegue la richiesta al modello Google Gemini, utilizzando il modello più potente disponibile.
    """
    if not question or question.strip() == "":
        logging.error("La domanda fornita a gemini_query è vuota.")
        return "Non ho ricevuto alcuna domanda da parte tua."

    if not API_KEYS["gemini"]:
        logging.error("Chiave API di Gemini non impostata.")
        return "La chiave API non è configurata."

    API_KEY = API_KEYS["gemini"]

    # Aggiorna la memoria con la domanda dell'utente
    conversation_memory.append({"role": "user", "text": question})

    # Costruisci la lista dei messaggi per il prompt
    messages = []
    # Aggiungi il persona_prompt come messaggio di sistema
    # messages.append({"author": "system", "content": persona_prompt})

    # for msg in conversation_memory:
    #     if msg["role"] == "user":
    #         messages.append({"author": "user", "content": msg["text"]})
    #     else:
    #         messages.append({"author": "assistant", "content": msg["text"]})

    # # Limita la lunghezza della conversazione, se necessario
    # MAX_MESSAGES = 10
    # if len(messages) > MAX_MESSAGES:
    #     messages = [messages[0]] + messages[-(MAX_MESSAGES-1):]  # Mantieni il persona_prompt e gli ultimi messaggi

    # Usa il modello più potente disponibile
    model_name = "models/gemini-1.5-pro"
    url = f"https://generativelanguage.googleapis.com/v1beta/models/{model_name}:generateContent?key={API_KEY}"
    headers = {
        "Content-Type": "application/json"
    }

    body = {
        "contents": [{
           "parts": [{
              "text": f"{persona_prompt} {question}"
           }]
          }],
        "generationConfig": {
              "temperature": 0.2,
             "topK": 40,
             "topP": 0.95
         }
    }

    try:
        response = requests.post(url, headers=headers, json=body, timeout=10)
        if response.status_code == 200:
           data = response.json()
           candidates = data.get("candidates", [])
           if candidates:
               answer = candidates[0].get("content").get("parts")[0].get("text")
           else:
               answer = "Nessuna risposta dal modello."
        else:
            logging.error(f"[ERRORE] gemini_query: {response.text}")
            answer = f"Errore nella richiesta al modello. Codice: {response.status_code}"

        # Salviamo la risposta nella cronologia
        conversation_memory.append({"role": "assistant", "text": answer})
        return answer

    except requests.exceptions.RequestException as e:
        logging.error(f"[ERRORE] gemini_query: {e}")
        return "Errore di rete o timeout nella richiesta al modello."

# -----------------------------
# LOGICA DEI COMANDI
# -----------------------------
def handle_command(command):
    """
    Se la frase contiene una parola di attivazione, esegue sub-comandi;
    altrimenti risponde con Gemini.
    """
    if is_activation_command(command):
        sub_cmd = take_command()
        if not sub_cmd or sub_cmd.strip() == "":
            speak("Non ho capito, puoi ripetere per favore?")
            return
        # Risposte molto semplici
        if maybe_personal_reply(sub_cmd):
            return
        # Controllo dispositivi smart
        if control_smart_device(sub_cmd):
            return
        # Comandi di uscita
        if "esci" in sub_cmd or "termina" in sub_cmd or "addio" in sub_cmd:
            speak("Va bene, alla prossima!")
            speech_queue.put(None)
            os._exit(0)
        elif "meteo" in sub_cmd:
            weather()
        elif "notizie" in sub_cmd:
            fetch_news()
        elif "risorse" in sub_cmd or "cpu" in sub_cmd or "memoria" in sub_cmd:
            monitor_resources()
        elif "apri" in sub_cmd or "website" in sub_cmd or "sito" in sub_cmd:
            open_webpage()
        elif "calendario" in sub_cmd:
            show_calendar()
        elif "converti" in sub_cmd or "convertitore" in sub_cmd:
            unit_converter()
        elif "promemoria" in sub_cmd:
            set_reminder()
        elif "barzelletta" in sub_cmd or "joke" in sub_cmd:
            tell_joke()
        elif "foto" in sub_cmd or "fotocamera" in sub_cmd:
            take_photo()
        elif "cerca" in sub_cmd and "google" in sub_cmd:
            search_google()
        elif "ore sono" in sub_cmd or "che ora" in sub_cmd:
            tell_time()
        elif "calcola" in sub_cmd or "calcolatrice" in sub_cmd:
            calculator()
        elif "todo" in sub_cmd or "cose da fare" in sub_cmd:
            manage_todo()
        elif "invia email" in sub_cmd or "manda email" in sub_cmd:
            send_email()
        elif "traduci" in sub_cmd or "traduzione" in sub_cmd:
            translate_text()
        else:
            # Se non è un comando conosciuto, chiediamo a Gemini
            answer = gemini_query(sub_cmd)
            speak(answer)
    else:
        # Se non ha usato la keyword di attivazione, ma è una frase generica
        if not command or command.strip() == "":
            return
        if maybe_personal_reply(command):
            return
        else:
            answer = gemini_query(command)
            speak(answer)

def assistant_loop():
    """
    Loop infinito che ascolta e crea thread per gestire i comandi.
    """
    while True:
        command = take_command()
        if command:
            t = threading.Thread(target=handle_command, args=(command,))
            t.start()

# -----------------------------
# PAGINA WEB CON LA SFERA 3D INTERATTIVA
# -----------------------------
HTML_PAGE = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>Gideon Assistant</title>
<style>
body { margin: 0; overflow: hidden; background: #000; }
#userText, #assistantText { position: absolute; bottom: 20px; width: 100%; text-align: center; z-index: 100; color: #fff; font-family: sans-serif; font-size: 24px; }
#assistantText { bottom: 50px; }
</style>
</head>
<body>

<!-- Three.js -->
<script src="https://cdn.jsdelivr.net/npm/three@0.146.0/build/three.min.js"></script>
<!-- Socket.IO -->
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>

<script>
let scene, camera, renderer, points;
let originalPositions = [];
let speaking = false;
let userTextElem, assistantTextElem;

const numParticles = 2000;
const radius = 2;

function init() {
    scene = new THREE.Scene();
    camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight, 0.1, 100);
    camera.position.z = 8;

    renderer = new THREE.WebGLRenderer({antialias: true});
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);

    // Particles geometry
    const geometry = new THREE.BufferGeometry();
    const positions = new Float32Array(numParticles * 3);

    for (let i = 0; i < numParticles; i++) {
        const phi = Math.random() * 2 * Math.PI;
        const costheta = Math.random() * 2 - 1;
        const theta = Math.acos(costheta);
        const r = radius;

        const x = r * Math.sin(theta) * Math.cos(phi);
        const y = r * Math.sin(theta) * Math.sin(phi);
        const z = r * Math.cos(theta);

        positions[i*3] = x;
        positions[i*3+1] = y;
        positions[i*3+2] = z;
        originalPositions.push({x, y, z});
    }

    geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));

    const material = new THREE.PointsMaterial({
        color: 0x00ffff,
        size: 0.04
    });

    points = new THREE.Points(geometry, material);
    scene.add(points);

    window.addEventListener('resize', onWindowResize, false);
    animate();

    // Elementi per visualizzare il testo
    userTextElem = document.createElement('div');
    userTextElem.id = 'userText';
    document.body.appendChild(userTextElem);

    assistantTextElem = document.createElement('div');
    assistantTextElem.id = 'assistantText';
    document.body.appendChild(assistantTextElem);
}

function onWindow
def onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
}

function animate() {
    requestAnimationFrame(animate);
    points.rotation.y += 0.001;
    disperseOrCompose();
    renderer.render(scene, camera);
}

function disperseOrCompose() {
    const positions = points.geometry.attributes.position.array;
    for (let i = 0; i < numParticles; i++) {
        const idx = i*3;
        let ox = originalPositions[i].x;
        let oy = originalPositions[i].y;
        let oz = originalPositions[i].z;

        if (speaking) {
            // Animazione più dinamica durante il parlato
            positions[idx] += (Math.random() - 0.5) * 0.05;
            positions[idx+1] += (Math.random() - 0.5) * 0.05;
            positions[idx+2] += (Math.random() - 0.5) * 0.05;
        } else {
            // Torna gradualmente alla posizione iniziale
            positions[idx] += (ox - positions[idx]) * 0.05;
            positions[idx+1] += (oy - positions[idx+1]) * 0.05;
            positions[idx+2] += (oz - positions[idx+2]) * 0.05;
        }
    }
    points.geometry.attributes.position.needsUpdate = true;
}

// Socket.IO
const socket = io();
socket.on('connect', () => {
    console.log('Connesso al server SocketIO.');
});

socket.on('speaking_status', data => {
    speaking = data.speaking;
    console.log("speaking =", speaking);
});

socket.on('user_speech', data => {
    // Visualizza il testo dell'utente
    userTextElem.textContent = 'Tu: ' + data.text;
});

socket.on('assistant_response', data => {
    // Visualizza il testo dell'assistente
    assistantTextElem.textContent = 'Gideon: ' + data.text;
});

init();
</script>
</body>
</html>
"""

@app.route("/")
def index():
    return HTML_PAGE

# -----------------------------
# MAIN
# -----------------------------
def main():
    global tts  # dichiarare tts come globale
    # Eseguiamo la funzione per elencare i modelli disponibili
    # Puoi commentare questa linea se non ti serve
    list_available_models()

    # Rimuoviamo il recupero dell'ID voce
    # VOICE_ID = get_voice_id("Bella")  # Voce femminile rilassante
    # if not VOICE_ID:
    #     print("[ERRORE] Non ho potuto caricare l'ID della voce. "
    #           "Controlla la tua chiave ElevenLabs o il nome della voce.")
    try:
        tts = UnleashedTTS(nb_accounts=2, create_accounts_threads=2)
    except Exception as e:
        logging.error(f"[ERRORE] Inizializzazione di UnleashedTTS: {e}")
        print("[ERRORE] Impossibile inizializzare UnleashedTTS, controlla la tua connessione e ChromeDriver.")
        os._exit(1)
    # Thread che esegue la sintesi vocale in background
    t_speak = threading.Thread(target=speak_worker, daemon=True)
    t_speak.start()

    # Thread che esegue il loop di ascolto/risposte
    t_assist = threading.Thread(target=assistant_loop, daemon=True)
    t_assist.start()

    # Frase iniziale
    speak("Ciao, sono Gideon. Fammi pure una domanda o dimmi un comando!")

    # Avviamo Flask-SocketIO
    socketio.run(app, host="127.0.0.1", port=5000, debug=False, use_reloader=False)

if __name__ == "__main__":
    main()