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

# -----------------------------
# 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": "",  # Sostituisci con la tua chiave API ElevenLabs
    "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
# -----------------------------
VOICE_ID = None
speech_queue = queue.Queue()

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, VOICE_ID
    IS_SPEAKING = True

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

    if not API_KEYS["elevenlabs"]:
        print("[ERRORE] Nessuna API Key ElevenLabs impostata, impossibile parlare.")
        IS_SPEAKING = False
        socketio.emit('speaking_status', {'speaking': False})
        return

    # Se non abbiamo ancora recuperato l'ID voce, proviamo ora
    if not VOICE_ID:
        VOICE_ID = get_voice_id("Bella")  # Voce femminile rilassante
        if not VOICE_ID:
            print("[ERRORE] Impossibile reperire l'ID della voce (ElevenLabs).")
            IS_SPEAKING = False
            socketio.emit('speaking_status', {'speaking': False})
            return

    # 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

    headers = {
        "xi-api-key": API_KEYS["elevenlabs"],
        "Content-Type": "application/json"
    }

    # Parametri TTS: voce più calma e rilassante
    voice_settings = {
        "stability": 0.85,          # Voce stabile
        "similarity_boost": 0.9     # Alta somiglianza con la voce campione
    }

    for chunk in chunks:
        payload = {
            "text": chunk,
            "model_id": "eleven_multilingual_v1",
            "voice_settings": voice_settings
        }
        try:
            url = f"https://api.elevenlabs.io/v1/text-to-speech/{VOICE_ID}"
            response = requests.post(url, headers=headers, json=payload)
            if response.status_code == 200:
                audio = AudioSegment.from_file(io.BytesIO(response.content), format="mp3")
                play(audio)
            else:
                logging.error(f"[ERRORE] TTS: {response.text}")
        except Exception as e:
            logging.error(f"[ERRORE] speak: {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-002"
    url = f"https://generativelanguage.googleapis.com/v1beta3/{model_name}:generateMessage?key={API_KEY}"
    headers = {
        "Content-Type": "application/json"
    }

    body = {
        "prompt": {
            "messages": messages
        },
        "temperature": 0.2,
        "candidate_count": 1,
        "top_k": 40,
        "top_p": 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", "")
            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 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 VOICE_ID

    # Eseguiamo la funzione per elencare i modelli disponibili
    # Puoi commentare questa linea se non ti serve
    list_available_models()

    # Recuperiamo l'ID della voce ElevenLabs
    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.")

    # 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()  ERROR:root:[ERRORE] gemini_query: {
  "error": {
    "code": 400,
    "message": "* GenerateMessageRequest.model: unexpected model name format\n",
    "status": "INVALID_ARGUMENT"
  }
}
 prova a sistemarlo con queste modifiche qua Okay, let's break down the error and get your code working. The core problem is with how you're specifying the Gemini model name in your code, specifically with the new Gemini Pro 1.5.

Understanding the Error

The error message:

{
  "error": {
    "code": 400,
    "message": "* GenerateMessageRequest.model: unexpected model name format\n",
    "status": "INVALID_ARGUMENT"
  }
}
Use code with caution.
Json
indicates that the model field in the request body doesn't conform to the expected format. Gemini Pro 1.5 models now have a specific naming convention, which your code is not using correctly. The problem is in your Gemini model URL call.

The Fix

You need to use this URL:

https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro:generateContent?key={API_KEY}
Use code with caution.
instead of:

f"https://generativelanguage.googleapis.com/v1beta3/{model_name}:generateMessage?key={API_KEY}"
Use code with caution.
And you need to rewrite your payload to:

body = {
        "contents": [{
           "parts": [{
              "text": f"{persona_prompt} {question}"
           }]
          }],
        "generationConfig": {
              "temperature": 0.2,
             "topK": 40,
             "topP": 0.95
         }
    }
Use code with caution.
The new endpoint generateContent expects to receive your prompt using contents and also your generation parameters need to be placed into a generationConfig key.

Here's the updated gemini_query function:

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."
Use code with caution.
Python
Key Changes

Model Endpoint: Changed to the correct endpoint for gemini-1.5-pro:

https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro:generateContent?key={API_KEY}
Use code with caution.
body Request: The format of the request body has been changed to match the new endpoint:

The prompt is sent as a content, with its parts as text

Generation parameters are placed into generationConfig key

Parsing the Response The parsing for candidates has been changed to access the response data in the correct way

Memory Functionality Removed: The memory functionality has been removed from the Gemini call. You can add this functionality back in the future, but it can be tricky. You should focus on making the main loop work, and then consider implementing the chat history.

How to Implement the Fix

Replace: Copy the modified gemini_query function and replace the existing function in your code.

Run: Execute your script.

Additional Notes

API Key: Ensure you have a valid Gemini API key in your API_KEYS dictionary.

Error Handling: The updated code provides basic error handling but make sure to improve it when you're ready to implement the conversation history.

Model Selection: Gemini 1.5 Pro may have a different pricing model, so check Google's documentation before heavy usage.

Testing

After the changes, run the script. It should correctly send the prompt to Gemini and retrieve the response.

Important Considerations

Gemini Model Name: Double-check the exact model name you intend to use from the Gemini API docs, especially with upcoming updates, to make sure it's compatible with the generateContent endpoint.

Rate Limiting: Be mindful of your API usage rate limits with Google's APIs to avoid errors.

Let me know how this works for you! If you still encounter issues, please paste the full traceback so I can better understand the error.