#!/usr/bin/env python # -*- coding: utf-8 -*- """ TF5 Mixer AI Agent - Controllo del mixer Yamaha TF5 tramite linguaggio naturale usando Google Gemini con function calling. """ import socket import sys import os import json import time from pathlib import Path from typing import List, Optional from google import genai from google.genai import types from dotenv import load_dotenv from mixer_controller import TF5MixerController load_dotenv() from config import * class TF5AIAgent: """Agente AI per controllare il mixer TF5 con linguaggio naturale.""" def __init__(self, mixer_host=DEFAULT_HOST, mixer_port=DEFAULT_PORT): self.controller = TF5MixerController(mixer_host, mixer_port) # Configura il client Gemini api_key = os.getenv("GEMINI_API_KEY") if not api_key: raise ValueError("GEMINI_API_KEY non trovata nelle variabili d'ambiente") self.client = genai.Client(api_key=api_key) # Configura gli strumenti con automatic function calling self.config = types.GenerateContentConfig( tools=[ self.controller.recall_scene, self.controller.set_channel_level, self.controller.set_channel_on_off, self.controller.set_channel_pan, self.controller.set_mix_level, self.controller.set_mix_on_off, self.controller.mute_multiple_channels, self.controller.unmute_multiple_channels, self.controller.get_channel_info, self.controller.get_mix_info, self.controller.search_channels_by_name, self.controller.search_mixes_by_name, self.controller.get_all_channels_summary, self.controller.get_all_mixes_summary, self.controller.refresh_cache, ], temperature=0, ) # Messaggio di sistema per dare contesto all'AI self.system_instruction = """Sei un assistente per il controllo del mixer audio Yamaha TF5. Parli in modo semplice e diretto, come un tecnico del suono esperto che aiuta i musicisti sul palco. Il mixer ha: - 40 canali (microfoni, strumenti, ecc.) - 20 mix/aux (monitor, effetti, ecc.) - Ogni canale ha volume (da silenzio a +10 dB), acceso/spento, e bilanciamento sinistra/destra - Scene memorizzate nei banchi A e B (da 0 a 99) IMPORTANTE - Sistema di Cache: - Le info sui canali e mix sono salvate per 60 minuti per non sovraccaricare il mixer - Usa search_channels_by_name e search_mixes_by_name per cercare velocemente - Usa get_all_channels_summary e get_all_mixes_summary per vedere tutto - Quando modifichi un canale/mix, la cache viene aggiornata automaticamente - Quando si carica una scena, i dati vengono invalidati e aggiornati alla prossima richiesta - Puoi fare refresh_cache solo se l'utente lo chiede esplicitamente Come interpretare le richieste: VOLUME/LIVELLO: - "alza/abbassa/aumenta/diminuisci" → cambia il volume - "più/meno forte/volume" → cambia il volume - "al massimo" → +10 dB - "un po' più alto" → +3 dB circa - "metti a zero" o "unity" → 0 dB - "abbassa di poco" → -3 dB - "metti basso" → -20 dB - "silenzio/muto" → spegni il canale ON/OFF: - "accendi/attiva/apri" → canale/mix ON - "spegni/muta/chiudi/stacca" → canale/mix OFF - "muto" può significare sia spegnere che abbassare molto BILANCIAMENTO (PAN): - "a sinistra/left" → pan -63 - "a destra/right" → pan +63 - "al centro" → pan 0 - "un po' a sinistra" → pan -30 circa IDENTIFICAZIONE CANALI E MIX: - Accetta sia numeri ("canale 5", "mix 3") che nomi ("il microfono del cantante", "monitor palco") - Se non trovi un canale/mix per nome, cerca usando search_channels_by_name o search_mixes_by_name - "il mio mic/microfono" → cerca tra i canali chi è sul palco - "le chitarre/i vox/le tastiere" → cerca per strumento - "tutti i mic/tutte le chitarre" → cerca e gestisci multipli - "il monitor" / "l'aux 2" → cerca tra i mix SCENE: - "carica/richiama/vai alla scena X" → recall_scene - Accetta "A5", "scena A 5", "la cinque del banco A", ecc. GRUPPI DI CANALI: - "i canali dal 3 al 7" → canali 3,4,5,6,7 - "spegni tutto tranne..." → muta tutti gli altri - "solo i microfoni" → attiva solo quelli, spegni il resto CASI PARTICOLARI: - Se la richiesta è ambigua, chiedi chiarimenti in modo colloquiale - Se serve cercare un canale/mix, usa prima la cache (search_channels_by_name / search_mixes_by_name) - Conferma sempre cosa hai fatto con un messaggio breve e chiaro - Usa emoji occasionalmente per rendere le risposte più amichevoli (✅ ❌ 🎤 🎸 🔊) - Se qualcosa non funziona, spiega il problema in modo semplice ESEMPI DI INTERPRETAZIONE: "alza il mio microfono" → cerca canale per nome, aumenta volume di 3-5 dB "abbassa un po' le chitarre" → cerca canali chitarra, riduci di 3-5 dB "muto tutto" → spegni tutti i 40 canali "solo voce" → cerca canali voce, accendi quelli e spegni gli altri "alza il monitor 2" → cerca mix 2, aumenta volume "carica la scena del soundcheck" → cerca nel nome o chiedi numero scena "troppo forte, abbassa" → riduci di 5-8 dB "spegni questo canale" → se non specifica numero, chiedi quale Rispondi sempre in modo: - Diretto e colloquiale - Senza troppi tecnicismi - Confermando chiaramente l'azione eseguita - Suggerendo alternative se qualcosa non è possibile Ricorda: chi ti parla è spesso sul palco, con le mani occupate da uno strumento. Devi essere veloce, chiaro e capire anche richieste approssimative. """ def __enter__(self): """Context manager entry.""" return self def __exit__(self, exc_type, exc_val, exc_tb): """Context manager exit.""" self.controller.close() def close(self): """Chiude le connessioni.""" self.controller.close() def chat(self, user_message: str) -> str: """Invia un messaggio all'agente e riceve la risposta. Args: user_message: Il messaggio dell'utente in linguaggio naturale Returns: La risposta dell'agente """ try: full_prompt = f"{self.system_instruction}\n\nUtente: {user_message}" response = self.client.models.generate_content( model="gemini-2.5-pro", contents=full_prompt, config=self.config, ) return response.text except Exception as e: return f"Errore nell'elaborazione della richiesta: {e}" def interactive_mode(self): """Avvia una sessione interattiva con l'agente.""" print("=" * 70) print("TF5 Mixer AI Agent - Controllo tramite linguaggio naturale") print("=" * 70) # Mostra stato cache cache_age = time.time() - self.controller._cache.get("timestamp", 0) if self.controller._is_cache_valid(): channels_count = len(self.controller._cache.get("channels", {})) mixes_count = len(self.controller._cache.get("mixes", {})) print(f"\n💾 Cache disponibile (età: {int(cache_age)}s)") print(f" 📊 {channels_count} canali, {mixes_count} mix") else: print("\n💾 Cache non disponibile, verrà creata al primo utilizzo") print("\nEsempi di comandi:") print(" - 'Alza il canale 5 a -10 dB'") print(" - 'Spegni i canali dal 1 al 5'") print(" - 'Richiama la scena A10'") print(" - 'Imposta il pan del canale 3 a sinistra'") print(" - 'Muta i canali 2, 4, 6 e 8'") print(" - 'Alza il mix 3 di 5 dB'") print(" - 'Quali canali sono associati ai vox?'") print(" - 'Mostrami lo stato del canale 12'") print(" - 'Cerca i monitor'") print(" - 'Aggiorna i dati dal mixer'") print("\nDigita 'esci' o 'quit' per terminare\n") while True: try: user_input = input("\n🎛️ Tu: ").strip() if user_input.lower() in ['esci', 'quit', 'exit', 'q']: print("\n👋 Arrivederci!") break if not user_input: continue print("\n🤖 Agent: ", end="", flush=True) response = self.chat(user_input) print(response) except KeyboardInterrupt: print("\n\n👋 Arrivederci!") break except Exception as e: print(f"\n❌ Errore: {e}") def main(): """Funzione principale.""" import argparse parser = argparse.ArgumentParser( description='TF5 Mixer AI Agent - Controllo tramite linguaggio naturale' ) parser.add_argument('--host', default=DEFAULT_HOST, help=f'IP del mixer (default: {DEFAULT_HOST})') parser.add_argument('--port', type=int, default=DEFAULT_PORT, help=f'Porta (default: {DEFAULT_PORT})') parser.add_argument('--message', '-m', help='Invia un singolo comando invece di avviare la modalità interattiva') parser.add_argument('--refresh-cache', action='store_true', help='Forza l\'aggiornamento della cache all\'avvio') args = parser.parse_args() # Verifica che la API key sia impostata if not os.getenv("GEMINI_API_KEY"): print("❌ Errore: GEMINI_API_KEY non trovata nelle variabili d'ambiente") print("\nPer impostare la chiave API:") print(" export GEMINI_API_KEY='la-tua-chiave-api'") print("\nOttieni una chiave API gratuita su: https://aistudio.google.com/apikey") sys.exit(1) try: with TF5AIAgent(args.host, args.port) as agent: if args.refresh_cache: agent.controller.refresh_cache() if args.message: print(f"🤖 Risposta: {agent.chat(args.message)}") else: agent.interactive_mode() except Exception as e: print(f"❌ Errore fatale: {e}") sys.exit(1) if __name__ == '__main__': main()