267 lines
10 KiB
Python
267 lines
10 KiB
Python
#!/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() |