Compare commits

..

10 Commits

Author SHA1 Message Date
Nick
1975a6f1d0 fix requirements.txtx 2026-02-16 21:02:25 +01:00
Nick
729cb649fc add extra tools 2026-02-16 21:02:05 +01:00
Nick
14512aa3f4 tasks yaml 2026-02-16 20:50:34 +01:00
Nick
4d5119ab5a add service auto discovery 2026-02-08 20:22:50 +01:00
Nick
9173aed76f fix telegram ip 2026-02-08 20:14:29 +01:00
Nick
3f09ff2bd6 fix sync cache between bot and mixer 2025-11-10 19:56:47 +01:00
Nick
221e1ad318 fix setup process 2025-11-03 20:31:52 +01:00
Nick
4246d13339 dockerizzato 2025-11-03 20:15:23 +01:00
Nick
86fa13a1a0 analizza mix 2025-11-03 20:10:10 +01:00
Nick
77bd4fabb6 fixed -inf 2025-11-03 19:35:48 +01:00
25 changed files with 3265 additions and 1133 deletions

View File

@@ -18,7 +18,7 @@
"channel": 3,
"name": "Talkback",
"on": false,
"level_db": null,
"level_db": -60.0,
"pan": 0
},
"4": {
@@ -31,15 +31,15 @@
"5": {
"channel": 5,
"name": "Vox1",
"on": true,
"level_db": -1.1,
"on": false,
"level_db": null,
"pan": 0
},
"6": {
"channel": 6,
"name": "Vox2",
"on": true,
"level_db": -0.15,
"on": false,
"level_db": null,
"pan": 0
},
"7": {
@@ -59,50 +59,50 @@
"9": {
"channel": 9,
"name": "Kick",
"on": true,
"level_db": -5.2,
"on": false,
"level_db": null,
"pan": 0
},
"10": {
"channel": 10,
"name": "Snare",
"on": true,
"level_db": -9.95,
"on": false,
"level_db": null,
"pan": 0
},
"11": {
"channel": 11,
"name": "Tom 1",
"on": true,
"level_db": -16.6,
"on": false,
"level_db": null,
"pan": 0
},
"12": {
"channel": 12,
"name": "Tom 2",
"on": true,
"level_db": -18.0,
"on": false,
"level_db": null,
"pan": 0
},
"13": {
"channel": 13,
"name": "Tom3",
"on": true,
"level_db": -24.0,
"on": false,
"level_db": null,
"pan": 0
},
"14": {
"channel": 14,
"name": "Pan SX",
"on": true,
"level_db": -15.08,
"on": false,
"level_db": null,
"pan": 0
},
"15": {
"channel": 15,
"name": "Pan dx",
"on": true,
"level_db": -14.68,
"on": false,
"level_db": null,
"pan": 0
},
"16": {
@@ -123,21 +123,21 @@
"channel": 18,
"name": "Archetto",
"on": false,
"level_db": null,
"level_db": -76.0,
"pan": 0
},
"19": {
"channel": 19,
"name": "Vox 5",
"on": true,
"level_db": -0.1,
"on": false,
"level_db": null,
"pan": 0
},
"20": {
"channel": 20,
"name": "Tast",
"on": true,
"level_db": 0.0,
"on": false,
"level_db": null,
"pan": 0
},
"21": {
@@ -157,15 +157,15 @@
"23": {
"channel": 23,
"name": " Vox3",
"on": true,
"level_db": -0.2,
"on": false,
"level_db": null,
"pan": 0
},
"24": {
"channel": 24,
"name": "Chit cnt",
"on": true,
"level_db": -1.75,
"on": false,
"level_db": null,
"pan": 0
},
"25": {
@@ -185,7 +185,7 @@
"27": {
"channel": 27,
"name": "Vox 4",
"on": true,
"on": false,
"level_db": null,
"pan": 0
},
@@ -228,14 +228,14 @@
"channel": 33,
"name": "PC",
"on": true,
"level_db": -20.4,
"pan": -63
"level_db": -17.5,
"pan": 0
},
"34": {
"channel": 34,
"name": "PC",
"on": true,
"level_db": -20.4,
"level_db": -17.5,
"pan": 63
},
"35": {
@@ -286,7 +286,7 @@
"mix": 1,
"name": "Aux 1",
"on": true,
"level_db": -1.55
"level_db": -11.1
},
"2": {
"mix": 2,
@@ -298,37 +298,37 @@
"mix": 3,
"name": "Aux 3",
"on": true,
"level_db": -0.5
"level_db": 0.35
},
"4": {
"mix": 4,
"name": "Aux 4",
"on": true,
"level_db": -0.6
"level_db": 0.0
},
"5": {
"mix": 5,
"name": "batteria",
"on": true,
"level_db": 1.35
"level_db": -4.9
},
"6": {
"mix": 6,
"name": "Aux 6",
"on": true,
"level_db": -6.65
"level_db": -9.3
},
"7": {
"mix": 7,
"name": "Aux 7",
"on": true,
"level_db": -2.7
"level_db": -6.35
},
"8": {
"mix": 8,
"name": "Aux 8",
"on": true,
"level_db": -2.65
"level_db": -9.0
},
"9": {
"mix": 9,
@@ -382,13 +382,13 @@
"mix": 17,
"name": "Diretta",
"on": true,
"level_db": 0.0
"level_db": -1.0
},
"18": {
"mix": 18,
"name": "Diretta",
"on": true,
"level_db": 0.0
"level_db": -1.0
},
"19": {
"mix": 19,
@@ -403,5 +403,12 @@
"level_db": 6.0
}
},
"timestamp": 1762193941.3288717
"timestamp": 1770577942.7806144,
"steinch": {},
"fxrtn": {},
"dcas": {},
"matrices": {},
"stereo": {},
"mono": {},
"mute_masters": {}
}

View File

@@ -0,0 +1,4 @@
{
"ip": "192.168.1.59",
"timestamp": 1771271930.0475464
}

19
Dockerfile Normal file
View File

@@ -0,0 +1,19 @@
# Dockerfile
# 1. Usa un'immagine Python ufficiale e leggera come base
FROM python:3.11-slim
# 2. Imposta una directory di lavoro all'interno del container
WORKDIR /app
# 3. Copia il file delle dipendenze e installale
# Questo passaggio viene fatto prima per sfruttare la cache di Docker.
# Se requirements.txt non cambia, questo strato non verrà ricostruito.
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 4. Copia tutti i file del progetto nella directory di lavoro del container
COPY . .
# 5. Comando da eseguire quando il container viene avviato
CMD ["python", "telegram_bot.py"]

Binary file not shown.

407
cache/mixer_state.json vendored Normal file
View File

@@ -0,0 +1,407 @@
{
"channels": {
"1": {
"channel": 1,
"name": "Sconosciuto",
"on": false,
"level_db": 0.65,
"pan": 0
},
"2": {
"channel": 2,
"name": "Gelato",
"on": true,
"level_db": null,
"pan": 0
},
"3": {
"channel": 3,
"name": "Talkback",
"on": false,
"level_db": null,
"pan": 0
},
"4": {
"channel": 4,
"name": "Sconosciuto",
"on": false,
"level_db": null,
"pan": 0
},
"5": {
"channel": 5,
"name": "Vox1",
"on": true,
"level_db": -13.15,
"pan": 0
},
"6": {
"channel": 6,
"name": "Vox2",
"on": true,
"level_db": null,
"pan": 0
},
"7": {
"channel": 7,
"name": "Basso",
"on": false,
"level_db": null,
"pan": 0
},
"8": {
"channel": 8,
"name": "Sconosciuto",
"on": false,
"level_db": null,
"pan": 0
},
"9": {
"channel": 9,
"name": "Kick",
"on": true,
"level_db": -5.2,
"pan": 0
},
"10": {
"channel": 10,
"name": "Snare",
"on": true,
"level_db": -9.95,
"pan": 0
},
"11": {
"channel": 11,
"name": "Tom 1",
"on": true,
"level_db": -16.6,
"pan": 0
},
"12": {
"channel": 12,
"name": "Tom 2",
"on": true,
"level_db": -3.45,
"pan": 0
},
"13": {
"channel": 13,
"name": "Tom3",
"on": true,
"level_db": -24.0,
"pan": 0
},
"14": {
"channel": 14,
"name": "Pan SX",
"on": true,
"level_db": -15.08,
"pan": 0
},
"15": {
"channel": 15,
"name": "Pan dx",
"on": true,
"level_db": -14.68,
"pan": 0
},
"16": {
"channel": 16,
"name": "Sconosciuto",
"on": false,
"level_db": null,
"pan": 0
},
"17": {
"channel": 17,
"name": "Sconosciuto",
"on": false,
"level_db": null,
"pan": 0
},
"18": {
"channel": 18,
"name": "Archetto",
"on": false,
"level_db": null,
"pan": 0
},
"19": {
"channel": 19,
"name": "Vox 5",
"on": false,
"level_db": null,
"pan": 0
},
"20": {
"channel": 20,
"name": "Tast",
"on": true,
"level_db": -10.8,
"pan": 0
},
"21": {
"channel": 21,
"name": "Sconosciuto",
"on": false,
"level_db": null,
"pan": 0
},
"22": {
"channel": 22,
"name": "Sconosciuto",
"on": false,
"level_db": null,
"pan": 0
},
"23": {
"channel": 23,
"name": " Vox3",
"on": true,
"level_db": null,
"pan": 0
},
"24": {
"channel": 24,
"name": "Chit cnt",
"on": true,
"level_db": -0.5,
"pan": 0
},
"25": {
"channel": 25,
"name": "Chit dx",
"on": true,
"level_db": -22.7,
"pan": 0
},
"26": {
"channel": 26,
"name": "Sconosciuto",
"on": false,
"level_db": null,
"pan": 0
},
"27": {
"channel": 27,
"name": "Vox 4",
"on": false,
"level_db": null,
"pan": 0
},
"28": {
"channel": 28,
"name": "Sconosciuto",
"on": false,
"level_db": null,
"pan": 0
},
"29": {
"channel": 29,
"name": "Pad",
"on": false,
"level_db": null,
"pan": 0
},
"30": {
"channel": 30,
"name": "Sconosciuto",
"on": false,
"level_db": null,
"pan": 0
},
"31": {
"channel": 31,
"name": "Sconosciuto",
"on": false,
"level_db": null,
"pan": 0
},
"32": {
"channel": 32,
"name": "Sconosciuto",
"on": false,
"level_db": null,
"pan": 0
},
"33": {
"channel": 33,
"name": "PC",
"on": true,
"level_db": -17.5,
"pan": -63
},
"34": {
"channel": 34,
"name": "PC",
"on": true,
"level_db": -17.5,
"pan": 63
},
"35": {
"channel": 35,
"name": "ch35",
"on": true,
"level_db": null,
"pan": 0
},
"36": {
"channel": 36,
"name": "ch36",
"on": true,
"level_db": null,
"pan": 0
},
"37": {
"channel": 37,
"name": "ch37",
"on": true,
"level_db": null,
"pan": 0
},
"38": {
"channel": 38,
"name": "ch38",
"on": true,
"level_db": null,
"pan": 0
},
"39": {
"channel": 39,
"name": "ch39",
"on": true,
"level_db": null,
"pan": 0
},
"40": {
"channel": 40,
"name": "ch40",
"on": true,
"level_db": null,
"pan": 0
}
},
"mixes": {
"1": {
"mix": 1,
"name": "Aux 1",
"on": true,
"level_db": -1.55
},
"2": {
"mix": 2,
"name": "Aux 2",
"on": true,
"level_db": -0.05
},
"3": {
"mix": 3,
"name": "Aux 3",
"on": true,
"level_db": -0.5
},
"4": {
"mix": 4,
"name": "Aux 4",
"on": true,
"level_db": -0.6
},
"5": {
"mix": 5,
"name": "batteria",
"on": true,
"level_db": 1.35
},
"6": {
"mix": 6,
"name": "Aux 6",
"on": true,
"level_db": -6.65
},
"7": {
"mix": 7,
"name": "Aux 7",
"on": true,
"level_db": -2.7
},
"8": {
"mix": 8,
"name": "Aux 8",
"on": true,
"level_db": -2.65
},
"9": {
"mix": 9,
"name": "Aux 9/10",
"on": true,
"level_db": -8.1
},
"10": {
"mix": 10,
"name": "Aux 9/10",
"on": true,
"level_db": -8.1
},
"11": {
"mix": 11,
"name": "Aux Pulp",
"on": true,
"level_db": -10.3
},
"12": {
"mix": 12,
"name": "Aux Pulp",
"on": true,
"level_db": -10.3
},
"13": {
"mix": 13,
"name": "Aux13/14",
"on": false,
"level_db": -1.6
},
"14": {
"mix": 14,
"name": "Aux13/14",
"on": false,
"level_db": -1.6
},
"15": {
"mix": 15,
"name": "Aux15/16",
"on": true,
"level_db": 0.1
},
"16": {
"mix": 16,
"name": "Aux15/16",
"on": true,
"level_db": 0.1
},
"17": {
"mix": 17,
"name": "Diretta",
"on": true,
"level_db": 0.0
},
"18": {
"mix": 18,
"name": "Diretta",
"on": true,
"level_db": 0.0
},
"19": {
"mix": 19,
"name": "Traduzio",
"on": true,
"level_db": 6.0
},
"20": {
"mix": 20,
"name": "Traduzio",
"on": true,
"level_db": 6.0
}
},
"timestamp": 1762800269.2657
}

249
config.py
View File

@@ -1,13 +1,252 @@
import socket
import ipaddress
from pathlib import Path
from typing import Optional
import json
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
# Configurazione mixer
DEFAULT_HOST = "192.168.1.62"
DEFAULT_PORT = 49280
TF5_INPUT_CHANNELS = 40
TF5_MIX_BUSSES = 20
from pathlib import Path
# Configurazione cache
CACHE_DIR = Path(".tf5_mixer_cache")
CACHE_FILE = CACHE_DIR / "channels_cache.json"
CACHE_DURATION = 3600 # 60 minuti in secondi
IP_CACHE_FILE = CACHE_DIR / "mixer_ip.json"
CACHE_DURATION = 3600 # 60 minuti in secondi
IP_CACHE_DURATION = 300 # 5 minuti per l'IP
def log(message: str, level: str = "INFO"):
"""Stampa log formattato con timestamp"""
timestamp = time.strftime("%H:%M:%S")
symbols = {
"INFO": "",
"SUCCESS": "",
"ERROR": "",
"WARNING": "⚠️",
"SEARCH": "🔍",
"NETWORK": "🌐",
"CACHE": "📍",
"TIME": "⏱️"
}
symbol = symbols.get(level, "")
print(f"[{timestamp}] {symbol} {message}")
def get_local_network() -> str:
"""Ottiene la rete locale del sistema"""
log("Rilevamento rete locale in corso...", "NETWORK")
try:
# Crea un socket per ottenere l'IP locale
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
local_ip = s.getsockname()[0]
s.close()
# Converte in network (es: 192.168.1.0/24)
ip_parts = local_ip.split('.')
network = f"{ip_parts[0]}.{ip_parts[1]}.{ip_parts[2]}.0/24"
log(f"IP locale rilevato: {local_ip}", "INFO")
log(f"Rete da scansionare: {network}", "NETWORK")
return network
except Exception as e:
log(f"Errore nel rilevare la rete locale: {e}", "ERROR")
network = "192.168.1.0/24"
log(f"Uso rete di fallback: {network}", "WARNING")
return network
def check_port(ip: str, port: int, timeout: float = 0.5) -> bool:
"""Controlla se una porta è aperta su un IP"""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
result = sock.connect_ex((ip, port))
sock.close()
return result == 0
except Exception as e:
# Log solo per debug, commentabile se troppo verbose
# log(f"Errore su {ip}: {e}", "ERROR")
return False
def scan_network_for_mixer(port: int = DEFAULT_PORT, max_workers: int = 50) -> Optional[str]:
"""Scansiona la rete locale per trovare il mixer"""
log(f"Ricerca mixer sulla porta {port}...", "SEARCH")
start_time = time.time()
network = get_local_network()
# Genera lista di IP da scansionare
ip_network = ipaddress.ip_network(network, strict=False)
total_hosts = sum(1 for _ in ip_network.hosts())
log(f"Indirizzi da scansionare: {total_hosts}", "INFO")
log(f"Thread paralleli: {max_workers}", "INFO")
found_ip = None
scanned = 0
# Scansione parallela per velocità
with ThreadPoolExecutor(max_workers=max_workers) as executor:
future_to_ip = {
executor.submit(check_port, str(ip), port): str(ip)
for ip in ip_network.hosts()
}
log("Scansione in corso...", "SEARCH")
for future in as_completed(future_to_ip):
ip = future_to_ip[future]
scanned += 1
# Log di progresso ogni 50 IP
if scanned % 50 == 0:
log(f"Progresso: {scanned}/{total_hosts} IP scansionati", "INFO")
try:
if future.result():
found_ip = ip
elapsed = time.time() - start_time
log(f"MIXER TROVATO su {ip}:{port}", "SUCCESS")
log(f"Tempo di scansione: {elapsed:.2f}s", "TIME")
log(f"IP scansionati: {scanned}/{total_hosts}", "INFO")
# Cancella i task rimanenti
executor.shutdown(wait=False, cancel_futures=True)
break
except Exception as e:
log(f"Errore durante scansione di {ip}: {e}", "ERROR")
if not found_ip:
elapsed = time.time() - start_time
log(f"Nessun mixer trovato sulla porta {port}", "ERROR")
log(f"Tempo totale di scansione: {elapsed:.2f}s", "TIME")
log(f"IP scansionati: {scanned}/{total_hosts}", "INFO")
return found_ip
def load_cached_ip() -> Optional[str]:
"""Carica l'IP dalla cache se valido"""
log("Controllo cache IP...", "CACHE")
if not IP_CACHE_FILE.exists():
log("Nessuna cache trovata", "INFO")
return None
try:
with open(IP_CACHE_FILE, 'r') as f:
data = json.load(f)
cached_ip = data.get('ip')
cache_time = data.get('timestamp', 0)
age = time.time() - cache_time
log(f"Cache trovata: {cached_ip} (età: {int(age)}s)", "CACHE")
# Controlla se la cache è ancora valida
if age < IP_CACHE_DURATION:
log(f"Verifica raggiungibilità di {cached_ip}...", "CACHE")
# Verifica che l'IP sia ancora valido
if cached_ip and check_port(cached_ip, DEFAULT_PORT, timeout=1.0):
log(f"IP dalla cache valido: {cached_ip}", "SUCCESS")
return cached_ip
else:
log(f"IP dalla cache non raggiungibile", "WARNING")
else:
log(f"Cache scaduta (max {IP_CACHE_DURATION}s)", "WARNING")
except Exception as e:
log(f"Errore nel leggere cache IP: {e}", "ERROR")
return None
def save_ip_to_cache(ip: str):
"""Salva l'IP nella cache"""
log(f"Salvataggio IP in cache: {ip}", "CACHE")
CACHE_DIR.mkdir(exist_ok=True)
try:
with open(IP_CACHE_FILE, 'w') as f:
json.dump({
'ip': ip,
'timestamp': time.time()
}, f, indent=2)
log(f"Cache salvata: {IP_CACHE_FILE}", "SUCCESS")
except Exception as e:
log(f"Errore nel salvare cache IP: {e}", "ERROR")
def get_mixer_ip() -> str:
"""
Ottiene l'IP del mixer con auto-discovery:
1. Controlla cache
2. Scansiona la rete
3. Fallback su IP predefinito
"""
log("=== INIZIO RICERCA MIXER ===", "INFO")
# Prova con la cache
cached_ip = load_cached_ip()
if cached_ip:
log("=== RICERCA COMPLETATA (CACHE) ===", "SUCCESS")
return cached_ip
# Scansiona la rete
log("Avvio scansione di rete...", "SEARCH")
discovered_ip = scan_network_for_mixer()
if discovered_ip:
save_ip_to_cache(discovered_ip)
log("=== RICERCA COMPLETATA (DISCOVERY) ===", "SUCCESS")
return discovered_ip
# Fallback
fallback_ip = "192.168.1.57"
log(f"Uso IP predefinito di fallback: {fallback_ip}", "WARNING")
log("=== RICERCA COMPLETATA (FALLBACK) ===", "WARNING")
return fallback_ip
# Variabile globale per l'host (si aggiorna automaticamente)
DEFAULT_HOST = None
def get_host() -> str:
"""Ottiene l'host del mixer (con lazy loading)"""
global DEFAULT_HOST
if DEFAULT_HOST is None:
log("Host mixer non inizializzato, avvio discovery...", "INFO")
DEFAULT_HOST = get_mixer_ip()
else:
log(f"Host mixer già inizializzato: {DEFAULT_HOST}", "INFO")
return DEFAULT_HOST
def refresh_mixer_ip():
"""Forza un refresh dell'IP del mixer"""
global DEFAULT_HOST
log("=== REFRESH FORZATO IP MIXER ===", "INFO")
log("Cancellazione cache esistente...", "CACHE")
# Rimuovi cache
if IP_CACHE_FILE.exists():
try:
IP_CACHE_FILE.unlink()
log("Cache rimossa", "SUCCESS")
except Exception as e:
log(f"Errore rimozione cache: {e}", "ERROR")
# Nuova scansione
DEFAULT_HOST = scan_network_for_mixer()
if DEFAULT_HOST:
save_ip_to_cache(DEFAULT_HOST)
log(f"Nuovo IP impostato: {DEFAULT_HOST}", "SUCCESS")
else:
log("Refresh fallito, mantengo IP precedente", "ERROR")
log("=== REFRESH COMPLETATO ===", "INFO")
return DEFAULT_HOST
# Test rapido
if __name__ == "__main__":
log("=== TEST AUTO-DISCOVERY MIXER ===", "INFO")
host = get_host()
log(f"Host finale: {host}", "SUCCESS")
# Test refresh
# log("\n=== TEST REFRESH ===", "INFO")
# refresh_mixer_ip()

View File

@@ -0,0 +1,21 @@
{
"user_id": 1073121360,
"username": "Ingdavidfeliz",
"last_updated": 1762197549.52935,
"last_updated_str": "2025-11-03 19:19:09",
"message_count": 2,
"history": [
{
"role": "user",
"content": "Alzami il basso",
"timestamp": 1762197547.3495104,
"timestamp_str": "2025-11-03 19:19:07"
},
{
"role": "assistant",
"content": "Ciao David! Per \"alzami il basso\", intendi nel tuo monitor personale o in un altro mix? Ho bisogno di sapere in quale mix vuoi che alzi il basso.",
"timestamp": 1762197549.5293229,
"timestamp_str": "2025-11-03 19:19:09"
}
]
}

View File

@@ -0,0 +1,93 @@
{
"user_id": 309700430,
"username": "Cicciomt7",
"last_updated": 1762198991.2953947,
"last_updated_str": "2025-11-03 19:43:11",
"message_count": 14,
"history": [
{
"role": "user",
"content": "Chiudi vox 5",
"timestamp": 1762198915.897361,
"timestamp_str": "2025-11-03 19:41:55"
},
{
"role": "assistant",
"content": "Ok, Vox 5 (canale 19) è spento. 🎤❌",
"timestamp": 1762198920.8823922,
"timestamp_str": "2025-11-03 19:42:00"
},
{
"role": "user",
"content": "Apri vox 5",
"timestamp": 1762198929.1841967,
"timestamp_str": "2025-11-03 19:42:09"
},
{
"role": "assistant",
"content": "Ok, Vox 5 (canale 19) è acceso. 🎤✅",
"timestamp": 1762198931.5372686,
"timestamp_str": "2025-11-03 19:42:11"
},
{
"role": "user",
"content": "Chiusi vox 3",
"timestamp": 1762198935.3993735,
"timestamp_str": "2025-11-03 19:42:15"
},
{
"role": "assistant",
"content": "Non ho trovato nessun canale chiamato \"vox 3\". Sei sicuro del nome? Forse intendevi un altro canale o un numero?",
"timestamp": 1762198938.0440562,
"timestamp_str": "2025-11-03 19:42:18"
},
{
"role": "user",
"content": "Vox3",
"timestamp": 1762198948.152694,
"timestamp_str": "2025-11-03 19:42:28"
},
{
"role": "assistant",
"content": "Ok, \" Vox3\" (canale 23) è spento. 🎤❌",
"timestamp": 1762198953.3262546,
"timestamp_str": "2025-11-03 19:42:33"
},
{
"role": "user",
"content": "Apri vox3",
"timestamp": 1762198964.16481,
"timestamp_str": "2025-11-03 19:42:44"
},
{
"role": "assistant",
"content": "Ok, Vox3 (canale 23) è acceso. 🎤✅",
"timestamp": 1762198966.712192,
"timestamp_str": "2025-11-03 19:42:46"
},
{
"role": "user",
"content": "Chiudi il gelato",
"timestamp": 1762198972.835868,
"timestamp_str": "2025-11-03 19:42:52"
},
{
"role": "assistant",
"content": "Ok, \"Gelato\" (canale 2) è spento. 🎤❌",
"timestamp": 1762198975.563454,
"timestamp_str": "2025-11-03 19:42:55"
},
{
"role": "user",
"content": "Riaprilo",
"timestamp": 1762198989.0117595,
"timestamp_str": "2025-11-03 19:43:09"
},
{
"role": "assistant",
"content": "Ok, \"Gelato\" (canale 2) è acceso. 🎤✅",
"timestamp": 1762198991.295375,
"timestamp_str": "2025-11-03 19:43:11"
}
]
}

View File

@@ -0,0 +1,21 @@
{
"user_id": 436176546,
"username": "Gloriushhh",
"last_updated": 1762199858.7119617,
"last_updated_str": "2025-11-03 19:57:38",
"message_count": 2,
"history": [
{
"role": "user",
"content": "Alza il volume del mio canale",
"timestamp": 1762199853.7008705,
"timestamp_str": "2025-11-03 19:57:33"
},
{
"role": "assistant",
"content": "Non riesco a trovare il tuo canale 'VOX 2'. Sei sicuro che si chiami così? Potrebbe esserci un nome leggermente diverso o il canale non è presente.",
"timestamp": 1762199858.7119193,
"timestamp_str": "2025-11-03 19:57:38"
}
]
}

View File

@@ -1,87 +1,135 @@
{
"user_id": 4762864,
"username": "unnikked",
"last_updated": 1762194065.7709692,
"last_updated_str": "2025-11-03 19:21:05",
"message_count": 13,
"last_updated": 1770578542.7348943,
"last_updated_str": "2026-02-08 19:22:22",
"message_count": 21,
"history": [
{
"role": "user",
"content": "Me lo spegni il gelato",
"timestamp": 1761595234.0849943,
"timestamp_str": "2025-10-27 21:00:34"
"content": "Controlla ora",
"timestamp": 1769455908.2156205,
"timestamp_str": "2026-01-26 19:31:48"
},
{
"role": "assistant",
"content": "Fatto! Canale \"Gelato\" spento. ✅",
"timestamp": 1761595239.1608367,
"timestamp_str": "2025-10-27 21:00:39"
"content": "Allora, Nicola, ho controllato di nuovo il mixer dopo il refresh! Ecco cosa vedo:\n\n**Canali:**\nCi sono un bel po' di canali attivi e nominati:\n* **Gelato (Canale 2):** Acceso, volume a -11.7 dB\n* **Vox1 (Canale 5):** Acceso, volume a 0 dB\n* **Vox2 (Canale 6):** Acceso, volume a 6.3 dB\n* **Basso (Canale 7):** Acceso, volume a -2.15 dB\n* **Kick (Canale 9):** Acceso, volume a -0.35 dB\n* **Snare (Canale 10):** Acceso, volume a -1.6 dB\n* **Tom 1 (Canale 11):** Acceso, volume a -2.8 dB\n* **Tom 2 (Canale 12):** Acceso, volume a -0.8 dB\n* **Tom3 (Canale 13):** Acceso, volume a -3.05 dB\n* **Pan SX (Canale 14):** Acceso, volume a -3.85 dB\n* **Pan dx (Canale 15):** Acceso, volume a -4.3 dB\n* **Archetto (Canale 18):** Acceso, volume a 4.6 dB\n* **Vox 5 (Canale 19):** Acceso, volume a -4.65 dB\n* **Tast (Canale 20):** Acceso, volume a -1.6 dB\n* **Vox3 (Canale 23):** Acceso, volume a 3.05 dB\n* **Chit cnt (Canale 24):** Acceso, volume a -2.35 dB\n* **Chit dx (Canale 25):** Acceso, volume a -0.85 dB\n* **PC (Canale 33 e 34):** Accesi, volume a -43.2 dB\n\n**Canali che sono accesi ma con volume bassissimo (-120 dB), quindi praticamente muti:**\n* **Vox 4 (Canale 27)**\n* **ch35 (Canale 35)**\n* **ch36 (Canale 36)**\n* **ch37 (Canale 37)**\n* **ch38 (Canale 38)**\n* **ch39 (Canale 39)**\n* **ch40 (Canale 40)**\n\n**Canali spenti o sconosciuti:**\n* **Canale 1:** Sconosciuto, spento, volume a 0.65 dB\n* **Talkback (Canale 3):** Spento, volume a -120 dB\n* **Canale 4:** Sconosciuto, spento, volume a -120 dB\n* **Canale 8:** Sconosciuto, spento, volume a -120 dB\n* **Canale 16:** Sconosciuto, spento, volume a -120 dB\n* **Canale 17:** Sconosciuto, spento, volume a -120 dB\n* **Canale 21:** Sconosciuto, spento, volume a -120 dB\n* **Canale 22:** Sconosciuto, spento, volume a -120 dB\n* **Canale 26:** Sconosciuto, spento, volume a -120 dB\n* **Canale 28:** Sconosciuto, spento, volume a -120 dB\n* **Pad (Canale 29):** Spento, volume a -120 dB\n* **Canali dal 30 al 32:** Sconosciuti, spenti, volume a -120 dB\n\n**Mix:**\nAnche i mix sono belli attivi:\n* **Aux 1 (Mix 1):** Acceso, volume a -11.1 dB\n* **Aux 2 (Mix 2):** Acceso, volume a -0.05 dB\n* **Aux 3 (Mix 3):** Acceso, volume a 0.35 dB\n* **Aux 4 (Mix 4):** Acceso, volume a 0 dB\n* **batteria (Mix 5):** Acceso, volume a 1.35 dB\n* **Aux 6 (Mix 6):** Acceso, volume a 10 dB\n* **Aux 7 (Mix 7):** Acceso, volume a -9.75 dB\n* **Aux 8 (Mix 8):** Acceso, volume a -4.4 dB\n* **Aux 9/10 (Mix 9 e 10):** Accesi, volume a -8.1 dB\n* **Aux Pulp (Mix 11 e 12):** Accesi, volume a -10.3 dB\n* **Aux15/16 (Mix 15 e 16):** Accesi, volume a 0.1 dB\n* **Diretta (Mix 17 e 18):** Accesi, volume a -2 dB\n* **Traduzio (Mix 19 e 20):** Accesi, volume a 6 dB\n\n**Mix spenti:**\n* **Aux13/14 (Mix 13 e 14):** Spenti, volume a -1.6 dB\n\nLa cache è stata aggiornata 5 secondi fa. Sembra che ci sia un buon movimento sul mixer! Fammi sapere se vuoi che faccia qualche modifica. 🎛️🎤",
"timestamp": 1769455917.246521,
"timestamp_str": "2026-01-26 19:31:57"
},
{
"role": "user",
"content": "Mi accendi il gelato.",
"timestamp": 1761595383.8880353,
"timestamp_str": "2025-10-27 21:03:03"
"content": "ciao",
"timestamp": 1769766889.7665708,
"timestamp_str": "2026-01-30 09:54:49"
},
{
"role": "assistant",
"content": "Certo! Gelato acceso! 🍦✅",
"timestamp": 1761595387.27931,
"timestamp_str": "2025-10-27 21:03:07"
"content": "Ciao Nicola! Tutto bene? Dimmi pure se hai bisogno di qualcosa con il mixer! 🎛️🎤",
"timestamp": 1769766891.7504349,
"timestamp_str": "2026-01-30 09:54:51"
},
{
"role": "user",
"content": "Apaga el helado. Cappuccino.",
"timestamp": 1761595426.0112157,
"timestamp_str": "2025-10-27 21:03:46"
"content": "è acceso?",
"timestamp": 1769766900.066535,
"timestamp_str": "2026-01-30 09:55:00"
},
{
"role": "assistant",
"content": "Gelato spento! ✅\nNon ho trovato nessun canale chiamato \"Cappuccino\", sei sicuro del nome? 🤔",
"timestamp": 1761595431.5373232,
"timestamp_str": "2025-10-27 21:03:51"
"content": "Cosa intendi? Un canale, un mix, o qualcos'altro? 🧐",
"timestamp": 1769766901.4737165,
"timestamp_str": "2026-01-30 09:55:01"
},
{
"role": "user",
"content": "Okay, aprimi il gelato allora!",
"timestamp": 1762193935.418264,
"timestamp_str": "2025-11-03 19:18:55"
},
{
"role": "user",
"content": "Gli alzi il gelato?",
"timestamp": 1762193958.1497903,
"timestamp_str": "2025-11-03 19:19:18"
},
{
"role": "user",
"content": "Aprimi il giubbotto.",
"timestamp": 1762194045.3195145,
"timestamp_str": "2025-11-03 19:20:45"
"content": "proprio il mixer, lo riesci a raggiungere?",
"timestamp": 1769766912.3007145,
"timestamp_str": "2026-01-30 09:55:12"
},
{
"role": "assistant",
"content": "Non posso aprirti il giubbotto, non è un canale o un mix del mixer! 😄\n\nPosso aiutarti con i canali audio, i microfoni o i monitor, se vuoi.",
"timestamp": 1762194046.8080468,
"timestamp_str": "2025-11-03 19:20:46"
"content": "✅ Certo, Nicola! Ho appena fatto un refresh e ho ricaricato tutte le informazioni dal mixer. Sembra che sia bello sveglio e raggiungibile! 🎛️✨",
"timestamp": 1769768014.9555686,
"timestamp_str": "2026-01-30 10:13:34"
},
{
"role": "user",
"content": "Aprimi il gelato",
"timestamp": 1762194053.0858698,
"timestamp_str": "2025-11-03 19:20:53"
"content": "System check",
"timestamp": 1770577025.9354753,
"timestamp_str": "2026-02-08 18:57:05"
},
{
"role": "user",
"content": "System check",
"timestamp": 1770577350.9690044,
"timestamp_str": "2026-02-08 19:02:30"
},
{
"role": "user",
"content": "System check",
"timestamp": 1770577683.8676918,
"timestamp_str": "2026-02-08 19:08:03"
},
{
"role": "user",
"content": "System check",
"timestamp": 1770577775.3157346,
"timestamp_str": "2026-02-08 19:09:35"
},
{
"role": "user",
"content": "System check",
"timestamp": 1770577899.7554913,
"timestamp_str": "2026-02-08 20:11:39"
},
{
"role": "user",
"content": "System check",
"timestamp": 1770577940.3210933,
"timestamp_str": "2026-02-08 20:12:20"
},
{
"role": "assistant",
"content": "Certo! Gelato acceso! 🍦✅",
"timestamp": 1762194054.6322277,
"timestamp_str": "2025-11-03 19:20:54"
"content": "✅ Fatto, Nicola! Ho appena fatto un controllo completo del sistema e ricaricato tutte le informazioni dal mixer. È bello sveglio e raggiungibile! 🎛️✨",
"timestamp": 1770577943.5131805,
"timestamp_str": "2026-02-08 20:12:23"
},
{
"role": "user",
"content": "È ancora chiuso",
"timestamp": 1762194065.7709692,
"timestamp_str": "2025-11-03 19:21:05"
"content": "Ciao",
"timestamp": 1770577995.903267,
"timestamp_str": "2026-02-08 19:13:15"
},
{
"role": "assistant",
"content": "Ciao Nicola! Tutto bene? Dimmi pure se hai bisogno di qualcosa con il mixer! 🎛️🎤",
"timestamp": 1770577997.697525,
"timestamp_str": "2026-02-08 19:13:17"
},
{
"role": "user",
"content": "Ciao pupo",
"timestamp": 1770578526.691519,
"timestamp_str": "2026-02-08 19:22:06"
},
{
"role": "assistant",
"content": "Ciao Nicola! Tutto bene? Dimmi pure se hai bisogno di qualcosa con il mixer! 🎛️🎤",
"timestamp": 1770578528.1236699,
"timestamp_str": "2026-02-08 19:22:08"
},
{
"role": "user",
"content": "Check canali",
"timestamp": 1770578537.6022112,
"timestamp_str": "2026-02-08 19:22:17"
},
{
"role": "assistant",
"content": "Allora, Nicola, ho controllato tutti i canali! Ecco la situazione attuale:\n\n**Canali Attivi (o con volume udibile):**\n* **PC (Canale 33 e 34):** Accesi, volume a -17.5 dB\n\n**Canali che sono accesi ma con volume bassissimo (-120 dB), quindi praticamente muti:**\n* **Gelato (Canale 2)**\n* **Vox1 (Canale 5)**\n* **Vox2 (Canale 6)**\n* **Basso (Canale 7)**\n* **Kick (Canale 9)**\n* **Snare (Canale 10)**\n* **Tom 1 (Canale 11)**\n* **Tom 2 (Canale 12)**\n* **Tom3 (Canale 13)**\n* **Pan SX (Canale 14)**\n* **Pan dx (Canale 15)**\n* **Vox 5 (Canale 19)**\n* **Tast (Canale 20)**\n* **Vox3 (Canale 23)**\n* **Chit cnt (Canale 24)**\n* **Chit dx (Canale 25)**\n* **Vox 4 (Canale 27)**\n* **ch35 (Canale 35)**\n* **ch36 (Canale 36)**\n* **ch37 (Canale 37)**\n* **ch38 (Canale 38)**\n* **ch39 (Canale 39)**\n* **ch40 (Canale 40)**\n\n**Canali spenti o sconosciuti:**\n* **Canale 1:** Sconosciuto, spento, volume a 0.65 dB\n* **Talkback (Canale 3):** Spento, volume a -60 dB\n* **Canale 4:** Sconosciuto, spento, volume a -120 dB\n* **Canale 8:** Sconosciuto, spento, volume a -120 dB\n* **Canale 16:** Sconosciuto, spento, volume a -120 dB\n* **Canale 17:** Sconosciuto, spento, volume a -120 dB\n* **Archetto (Canale 18):** Spento, volume a -76 dB\n* **Canale 21:** Sconosciuto, spento, volume a -120 dB\n* **Canale 22:** Sconosciuto, spento, volume a -120 dB\n* **Canale 26:** Sconosciuto, spento, volume a -120 dB\n* **Canale 28:** Sconosciuto, spento, volume a -120 dB\n* **Pad (Canale 29):** Spento, volume a -120 dB\n* **Canali dal 30 al 32:** Sconosciuti, spenti, volume a -120 dB\n\nLa cache è stata aggiornata circa 10 minuti fa. Sembra che molti canali siano spenti o con volumi molto bassi. Fammi sapere se vuoi che accenda o alzi qualcosa! 🎛️🎤",
"timestamp": 1770578542.7348733,
"timestamp_str": "2026-02-08 19:22:22"
}
]
}

35
docker-compose.yaml Normal file
View File

@@ -0,0 +1,35 @@
# docker-compose.yml
version: '3.8'
services:
tf5-bot:
# Costruisce l'immagine usando il Dockerfile nella cartella corrente
build: .
# Nome del container per una facile identificazione
container_name: tf5-telegram-bot
# Carica le variabili d'ambiente dal file .env
env_file:
- .env
# Politica di riavvio:
# 'unless-stopped' riavvia il container automaticamente all'avvio di Docker
# (e in caso di crash), a meno che tu non lo abbia fermato manualmente.
restart: unless-stopped
# Modalità di rete "host":
# Questo è FONDAMENTALE. Permette al container di accedere direttamente
# alla rete del tuo computer. Senza questo, il bot non potrebbe
# "vedere" e connettersi al mixer TF5 che si trova sulla tua rete locale.
network_mode: "host"
# Volumi:
# Questo collega le cartelle del tuo computer (a sinistra dei due punti)
# alle cartelle all'interno del container (a destra).
# In questo modo, i profili, le conversazioni e la cache vengono salvati
# sul tuo disco e non vengono persi se il container viene riavviato.
volumes:
- ./profiles:/app/profiles
- ./conversations:/app/conversations
- ./cache:/app/cache

View File

@@ -0,0 +1,12 @@
{
"channels": {},
"mixes": {},
"stereo_ins": {},
"fx_returns": {},
"dcas": {},
"matrices": {},
"stereo_out": {},
"mono_out": {},
"mute_masters": {},
"timestamp": 0
}

File diff suppressed because it is too large Load Diff

17
profiles/1552084582.json Normal file
View File

@@ -0,0 +1,17 @@
{
"user_id": 1552084582,
"username": "laissaantuness",
"role": "musicista",
"display_name": "Lai",
"channel_labels": [
"Chitarra"
],
"mix_labels": [
"Aux 7"
],
"setup_completed": true,
"created_at": 1762199748.9965794,
"created_at_str": "2025-11-03 19:55:48",
"last_updated": 1762199837.8484952,
"last_updated_str": "2025-11-03 19:57:17"
}

13
profiles/309700430.json Normal file
View File

@@ -0,0 +1,13 @@
{
"user_id": 309700430,
"username": "Cicciomt7",
"role": "mixerista",
"display_name": "Frank",
"channel_labels": [],
"mix_labels": [],
"setup_completed": true,
"created_at": 1762198859.8032317,
"created_at_str": "2025-11-03 19:40:59",
"last_updated": 1762198880.1237595,
"last_updated_str": "2025-11-03 19:41:20"
}

17
profiles/392578198.json Normal file
View File

@@ -0,0 +1,17 @@
{
"user_id": 392578198,
"username": "Unknown",
"role": "cantante",
"display_name": "Rox",
"channel_labels": [
"Vox1"
],
"mix_labels": [
"Aux2"
],
"setup_completed": true,
"created_at": 1762199748.3117115,
"created_at_str": "2025-11-03 19:55:48",
"last_updated": 1762199831.8709695,
"last_updated_str": "2025-11-03 19:57:11"
}

17
profiles/436176546.json Normal file
View File

@@ -0,0 +1,17 @@
{
"user_id": 436176546,
"username": "Gloriushhh",
"role": "cantante",
"display_name": "Gloriush",
"channel_labels": [
"VOX 2"
],
"mix_labels": [
"AUX 3"
],
"setup_completed": true,
"created_at": 1762199750.435028,
"created_at_str": "2025-11-03 19:55:50",
"last_updated": 1762199825.8800879,
"last_updated_str": "2025-11-03 19:57:05"
}

View File

@@ -3,11 +3,19 @@
"username": "unnikked",
"role": "mixerista",
"display_name": "Nicola",
"channels": [],
"mixes": [],
"channel_labels": [
"I canali della batteria (kick",
"drum",
"pan",
"tom",
"etc)"
],
"mix_labels": [
"Aux batteria"
],
"setup_completed": true,
"created_at": 1762193898.87283,
"created_at_str": "2025-11-03 19:18:18",
"last_updated": 1762193916.4840386,
"last_updated_str": "2025-11-03 19:18:36"
"created_at": 1762198047.365003,
"created_at_str": "2025-11-03 20:27:27",
"last_updated": 1762799485.4542227,
"last_updated_str": "2025-11-10 19:31:25"
}

17
profiles/540754896.json Normal file
View File

@@ -0,0 +1,17 @@
{
"user_id": 540754896,
"username": "merymoon001",
"role": "musicista",
"display_name": "Moon",
"channel_labels": [
"Tastiera"
],
"mix_labels": [
"aux 8"
],
"setup_completed": true,
"created_at": 1762199744.617781,
"created_at_str": "2025-11-03 19:55:44",
"last_updated": 1762199821.1137702,
"last_updated_str": "2025-11-03 19:57:01"
}

View File

@@ -1 +1,3 @@
dotenv
google-genai
pyyaml

246
task_runner.py Normal file
View File

@@ -0,0 +1,246 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
task_runner.py
==============
Esegue file di task YAML sul Yamaha TF5 tramite TF5MixerController.
Uso da CLI:
python task_runner.py tasks_concerto_live.yaml # lista le cue disponibili
python task_runner.py tasks_concerto_live.yaml CUE_01 # esegue una cue specifica
python task_runner.py tasks_concerto_live.yaml --all # esegue tutte le cue in sequenza
"""
import sys
import time
import yaml
from pathlib import Path
from typing import Optional
from mixer_controller import TF5MixerController
# ──────────────────────────────────────────────────────────────
# DISPATCHER: mappa action → metodo del controller
# ──────────────────────────────────────────────────────────────
class TaskRunner:
def __init__(self, controller: TF5MixerController):
self.ctrl = controller
# Mappa nome azione → callable
self._action_map = {
"set_channel_level": self._set_channel_level,
"set_channel_on_off": self._set_channel_on_off,
"set_channel_pan": self._set_channel_pan,
"mute_channels": self._mute_channels,
"unmute_channels": self._unmute_channels,
"set_mix_level": self._set_mix_level,
"set_mix_on_off": self._set_mix_on_off,
"set_channel_to_mix_level": self._set_channel_to_mix_level,
"set_channel_to_mix_on_off":self._set_channel_to_mix_on_off,
"recall_scene": self._recall_scene,
"wait": self._wait,
"refresh_cache": self._refresh_cache,
}
# ── Handlers specifici ──────────────────────────────────
def _set_channel_level(self, step: dict) -> dict:
return self.ctrl.set_channel_level(step["channel"], float(step["level_db"]))
def _set_channel_on_off(self, step: dict) -> dict:
return self.ctrl.set_channel_on_off(step["channel"], bool(step["state"]))
def _set_channel_pan(self, step: dict) -> dict:
return self.ctrl.set_channel_pan(step["channel"], int(step["pan"]))
def _mute_channels(self, step: dict) -> dict:
return self.ctrl.mute_multiple_channels(list(step["channels"]))
def _unmute_channels(self, step: dict) -> dict:
return self.ctrl.unmute_multiple_channels(list(step["channels"]))
def _set_mix_level(self, step: dict) -> dict:
return self.ctrl.set_mix_level(step["mix"], float(step["level_db"]))
def _set_mix_on_off(self, step: dict) -> dict:
return self.ctrl.set_mix_on_off(step["mix"], bool(step["state"]))
def _set_channel_to_mix_level(self, step: dict) -> dict:
return self.ctrl.set_channel_to_mix_level(step["channel"], step["mix"], float(step["level_db"]))
def _set_channel_to_mix_on_off(self, step: dict) -> dict:
return self.ctrl.set_channel_to_mix_on_off(step["channel"], step["mix"], bool(step["state"]))
def _recall_scene(self, step: dict) -> dict:
return self.ctrl.recall_scene(str(step["bank"]), int(step["scene"]))
def _wait(self, step: dict) -> dict:
ms = int(step.get("ms", 0))
print(f" ⏱ Wait puro: {ms} ms")
time.sleep(ms / 1000.0)
return {"status": "success", "message": f"Atteso {ms} ms"}
def _refresh_cache(self, step: dict) -> dict:
return self.ctrl.refresh_cache()
# ── Esecuzione di un singolo step ───────────────────────
def _run_step(self, step_num: int, step: dict) -> bool:
"""Esegue un singolo step. Ritorna True se OK, False se errore."""
action = step.get("action")
delay_ms = int(step.get("delay_ms", 0))
wait_ms = int(step.get("wait_ms", 0))
if delay_ms > 0:
print(f" ⏳ delay_ms={delay_ms} prima dell'azione...")
time.sleep(delay_ms / 1000.0)
handler = self._action_map.get(action)
if handler is None:
print(f" ⚠️ Step {step_num}: azione '{action}' non riconosciuta — saltato.")
return False
print(f" ▶ Step {step_num}: {action} | params: { {k:v for k,v in step.items() if k not in ('action','delay_ms','wait_ms')} }")
try:
result = handler(step)
status = result.get("status", "unknown")
msg = result.get("message", "")
icon = "" if status in ("success", "partial") else ""
print(f" {icon} [{status}] {msg}")
if wait_ms > 0:
time.sleep(wait_ms / 1000.0)
return status in ("success", "partial")
except Exception as e:
print(f" ❌ Eccezione durante '{action}': {e}")
return False
# ── Esecuzione di una cue ────────────────────────────────
def run_cue(self, cue: dict) -> bool:
"""
Esegue tutti gli step di una cue.
Ritorna True se tutti gli step sono andati a buon fine.
"""
cue_id = cue.get("id", "???")
name = cue.get("name", "")
on_error = cue.get("on_error", "stop")
steps = cue.get("steps", [])
print(f"\n{''*60}")
print(f"🎬 CUE: {cue_id}{name}")
if cue.get("description"):
print(f" 📋 {cue['description']}")
print(f" Steps: {len(steps)} | on_error: {on_error}")
print(f"{''*60}")
all_ok = True
for i, step in enumerate(steps, start=1):
ok = self._run_step(i, step)
if not ok:
all_ok = False
if on_error == "stop":
print(f"\n 🛑 on_error=stop: cue {cue_id} interrotta allo step {i}.")
return False
# on_error == "continue": vai avanti
result_icon = "" if all_ok else "⚠️ (con errori)"
print(f"\n {result_icon} Cue {cue_id} completata.")
return all_ok
# ──────────────────────────────────────────────────────────────
# LOADER YAML
# ──────────────────────────────────────────────────────────────
def load_task_file(path: str) -> dict:
"""Carica e valida un file di task YAML."""
p = Path(path)
if not p.exists():
raise FileNotFoundError(f"File non trovato: {path}")
with open(p, "r", encoding="utf-8") as f:
data = yaml.safe_load(f)
if "cues" not in data or not isinstance(data["cues"], list):
raise ValueError("Il file YAML deve contenere una lista 'cues'.")
return data
def print_event_info(data: dict):
event = data.get("event", {})
if event:
print(f"\n{''*60}")
print(f"📅 Evento : {event.get('name', 'N/D')}")
print(f" Data : {event.get('date', 'N/D')}")
print(f" Venue : {event.get('venue', 'N/D')}")
if event.get("notes"):
print(f" Note : {event['notes']}")
def list_cues(data: dict):
print_event_info(data)
print(f"\n{''*60}")
print("📋 CUE DISPONIBILI:")
print(f"{''*60}")
for cue in data["cues"]:
steps_count = len(cue.get("steps", []))
print(f" {cue.get('id'):12s}{cue.get('name','')} ({steps_count} steps)")
print(f"{''*60}\n")
# ──────────────────────────────────────────────────────────────
# ENTRY POINT
# ──────────────────────────────────────────────────────────────
def main():
if len(sys.argv) < 2:
print(__doc__)
sys.exit(1)
task_file = sys.argv[1]
cue_arg = sys.argv[2] if len(sys.argv) > 2 else None
try:
data = load_task_file(task_file)
except (FileNotFoundError, ValueError) as e:
print(f"❌ Errore nel caricamento del file: {e}")
sys.exit(1)
# Solo lista cue senza mixer
if cue_arg is None:
list_cues(data)
sys.exit(0)
# Connessione al mixer ed esecuzione
print("🔌 Connessione al mixer...")
import config
with TF5MixerController(host=config.get_host()) as ctrl:
runner = TaskRunner(ctrl)
if cue_arg == "--all":
print_event_info(data)
print("\n▶▶ Esecuzione di TUTTE le cue in sequenza...")
for cue in data["cues"]:
ok = runner.run_cue(cue)
if not ok and cue.get("on_error", "stop") == "stop":
print(f"\n🛑 Esecuzione interrotta a causa di un errore in {cue.get('id')}.")
sys.exit(1)
else:
# Cerca la cue per ID
cue_found = next((c for c in data["cues"] if c.get("id") == cue_arg), None)
if cue_found is None:
print(f"❌ Cue '{cue_arg}' non trovata nel file.")
list_cues(data)
sys.exit(1)
runner.run_cue(cue_found)
print("\n✅ Task runner completato.\n")
if __name__ == "__main__":
main()

71
task_schema.yaml Normal file
View File

@@ -0,0 +1,71 @@
# ============================================================
# SCHEMA DEI TASK - Yamaha TF5 Mixer Controller
# ============================================================
# Ogni file di task descrive UN evento (concerto, conferenza, ecc.)
# composto da N cues, ognuna con N steps eseguiti in sequenza.
#
# STRUTTURA GENERALE:
#
# event: → metadati dell'evento (opzionale)
# cues: → lista delle cue eseguibili
# - id: → identificatore univoco (es. "CUE_01")
# name: → nome leggibile
# description: → note operative (opzionale)
# on_error: → "stop" | "continue" (default: "stop")
# steps: → lista di azioni in sequenza
# - action: → tipo di azione (vedi sotto)
# ...params → parametri specifici dell'azione
# delay_ms: → attesa PRIMA di eseguire questo step (ms)
# wait_ms: → attesa DOPO l'esecuzione (ms)
#
# ============================================================
# TIPI DI AZIONE DISPONIBILI:
# ============================================================
#
# 1. set_channel_level
# channel: int (1-40)
# level_db: float (es. 0.0, -10.5, -inf → usa -999)
#
# 2. set_channel_on_off
# channel: int
# state: bool (true = ON, false = OFF / mute)
#
# 3. set_channel_pan
# channel: int
# pan: int (-63 = sinistra, 0 = centro, +63 = destra)
#
# 4. mute_channels
# channels: [int, int, ...]
#
# 5. unmute_channels
# channels: [int, int, ...]
#
# 6. set_mix_level
# mix: int (1-20)
# level_db: float
#
# 7. set_mix_on_off
# mix: int
# state: bool
#
# 8. set_channel_to_mix_level
# channel: int
# mix: int
# level_db: float
#
# 9. set_channel_to_mix_on_off
# channel: int
# mix: int
# state: bool
#
# 10. recall_scene
# bank: "a" | "b"
# scene: int (0-99)
#
# 11. wait
# ms: int → pausa pura senza azioni sul mixer
#
# 12. refresh_cache
# (nessun parametro) → forza un aggiornamento completo della cache
#
# ============================================================

238
tasks_concerto_live.yaml Normal file
View File

@@ -0,0 +1,238 @@
# ============================================================
# TASK FILE - Concerto Live "Band Example"
# Yamaha TF5 Mixer Controller
# ============================================================
event:
name: "Concerto Live - Band Example"
date: "2025-06-14"
venue: "Teatro Comunale"
notes: "FOH setup. Mix 1=IEM Vocalist, Mix 2=IEM Chitarra, Mix 3=Wedge Batteria"
cues:
# ----------------------------------------------------------
# PRE-SHOW: tutto silenzioso, scene base caricata
# ----------------------------------------------------------
- id: "CUE_01"
name: "Pre-Show - Carica scena base"
description: "Richiama la scena di partenza e porta tutto a zero"
on_error: "stop"
steps:
- action: recall_scene
bank: "a"
scene: 1
- action: wait
ms: 3000 # attendi che il mixer elabori la scena
- action: refresh_cache
wait_ms: 500
- action: mute_channels
channels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
- action: set_mix_on_off
mix: 1
state: false
- action: set_mix_on_off
mix: 2
state: false
- action: set_mix_on_off
mix: 3
state: false
# ----------------------------------------------------------
# SOUNDCHECK: apri i canali uno alla volta con fade-in
# ----------------------------------------------------------
- id: "CUE_02"
name: "Soundcheck - Fade-in canali batteria"
description: "Canali 1-4 (kick, snare, OH sx, OH dx) portati a -10 dB gradualmente"
on_error: "continue"
steps:
# Kick
- action: set_channel_level
channel: 1
level_db: -40.0
- action: set_channel_on_off
channel: 1
state: true
wait_ms: 200
- action: set_channel_level
channel: 1
level_db: -20.0
wait_ms: 200
- action: set_channel_level
channel: 1
level_db: -10.0
wait_ms: 300
# Snare
- action: set_channel_level
channel: 2
level_db: -40.0
delay_ms: 500
- action: set_channel_on_off
channel: 2
state: true
wait_ms: 200
- action: set_channel_level
channel: 2
level_db: -10.0
wait_ms: 300
# Overhead Sx + Dx insieme
- action: set_channel_level
channel: 3
level_db: -40.0
delay_ms: 500
- action: set_channel_level
channel: 4
level_db: -40.0
- action: set_channel_on_off
channel: 3
state: true
- action: set_channel_on_off
channel: 4
state: true
wait_ms: 200
- action: set_channel_pan
channel: 3
pan: -30 # OH sx → sinistra
- action: set_channel_pan
channel: 4
pan: 30 # OH dx → destra
wait_ms: 200
- action: set_channel_level
channel: 3
level_db: -14.0
- action: set_channel_level
channel: 4
level_db: -14.0
# ----------------------------------------------------------
# IEM MIX 1 (Vocalist): setup invii
# ----------------------------------------------------------
- id: "CUE_03"
name: "IEM Mix 1 - Setup invii vocalist"
description: "Configura il mix 1 per il vocalist: voce principale alta, resto basso"
on_error: "continue"
steps:
- action: set_mix_level
mix: 1
level_db: 0.0
- action: set_mix_on_off
mix: 1
state: true
wait_ms: 200
# Voce principale (ch 9) → Mix 1 alta
- action: set_channel_to_mix_level
channel: 9
mix: 1
level_db: 0.0
- action: set_channel_to_mix_on_off
channel: 9
mix: 1
state: true
# Chitarra ritmica (ch 5) → Mix 1 media
- action: set_channel_to_mix_level
channel: 5
mix: 1
level_db: -6.0
delay_ms: 100
- action: set_channel_to_mix_on_off
channel: 5
mix: 1
state: true
# Kick (ch 1) → Mix 1 bassa (solo click per tempo)
- action: set_channel_to_mix_level
channel: 1
mix: 1
level_db: -12.0
delay_ms: 100
- action: set_channel_to_mix_on_off
channel: 1
mix: 1
state: true
# ----------------------------------------------------------
# SHOW START: tutto ON, master a 0 dB
# ----------------------------------------------------------
- id: "CUE_04"
name: "SHOW START - Apri tutto"
description: "Porta tutti i canali attivi e il master a nominal"
on_error: "stop"
steps:
- action: unmute_channels
channels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
- action: set_mix_on_off
mix: 1
state: true
- action: set_mix_on_off
mix: 2
state: true
- action: set_mix_on_off
mix: 3
state: true
- action: recall_scene
bank: "a"
scene: 10 # scena "SHOW" pre-salvata
wait_ms: 2000
- action: refresh_cache
# ----------------------------------------------------------
# PAUSA: muta rapidamente tutto il palco
# ----------------------------------------------------------
- id: "CUE_05"
name: "PAUSA - Muta palco"
description: "Muta tutti i canali di palco, lascia accesa la diffusione BG music"
on_error: "continue"
steps:
- action: mute_channels
channels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
- action: set_channel_on_off # ch 16 = BG music playback
channel: 16
state: true
delay_ms: 200
- action: set_channel_level
channel: 16
level_db: -6.0
# ----------------------------------------------------------
# FINE SHOW: fade-out master in 5 step da 1 secondo
# ----------------------------------------------------------
- id: "CUE_06"
name: "FINE SHOW - Fade out generale"
description: "Abbassa tutti i canali gradualmente in 5 secondi"
on_error: "continue"
steps:
- action: set_channel_level
channel: 9
level_db: -6.0
wait_ms: 1000
- action: set_channel_level
channel: 9
level_db: -12.0
wait_ms: 1000
- action: set_channel_level
channel: 9
level_db: -20.0
wait_ms: 1000
- action: set_channel_level
channel: 9
level_db: -40.0
wait_ms: 1000
- action: mute_channels
channels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 16]
- action: recall_scene
bank: "a"
scene: 0 # scena vuota / safe
delay_ms: 500

View File

@@ -63,8 +63,8 @@ class UserProfile:
self.username = username
self.role: Optional[str] = None
self.display_name: Optional[str] = None
self.channels: List[int] = [] # Canali assegnati
self.mixes: List[int] = [] # Mix/aux assegnati
self.channel_labels: List[str] = [] # Etichette dei canali assegnati
self.mix_labels: List[str] = [] # Etichette dei mix/aux assegnati
self.setup_completed = False
self.created_at = time.time()
self.last_updated = time.time()
@@ -76,8 +76,8 @@ class UserProfile:
"username": self.username,
"role": self.role,
"display_name": self.display_name,
"channels": self.channels,
"mixes": self.mixes,
"channel_labels": self.channel_labels,
"mix_labels": self.mix_labels,
"setup_completed": self.setup_completed,
"created_at": self.created_at,
"created_at_str": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(self.created_at)),
@@ -91,8 +91,8 @@ class UserProfile:
profile = cls(data["user_id"], data["username"])
profile.role = data.get("role")
profile.display_name = data.get("display_name")
profile.channels = data.get("channels", [])
profile.mixes = data.get("mixes", [])
profile.channel_labels = data.get("channel_labels", [])
profile.mix_labels = data.get("mix_labels", [])
profile.setup_completed = data.get("setup_completed", False)
profile.created_at = data.get("created_at", time.time())
profile.last_updated = data.get("last_updated", time.time())
@@ -102,36 +102,6 @@ class UserProfile:
"""Restituisce le info sul ruolo."""
return self.ROLES.get(self.role, {})
def can_access_channel(self, channel: int) -> bool:
"""Verifica se l'utente può accedere a un canale."""
if not self.role:
return False
permissions = self.get_role_info().get("permissions", [])
if "all" in permissions:
return True
if "own_channel" in permissions:
return channel in self.channels
return False
def can_access_mix(self, mix: int) -> bool:
"""Verifica se l'utente può accedere a un mix."""
if not self.role:
return False
permissions = self.get_role_info().get("permissions", [])
if "all" in permissions:
return True
if "own_mix" in permissions:
return mix in self.mixes
return False
def get_summary(self) -> str:
"""Restituisce un riepilogo del profilo."""
if not self.setup_completed:
@@ -140,15 +110,14 @@ class UserProfile:
role_info = self.get_role_info()
msg = f"{role_info['emoji']} **{self.display_name}** ({role_info['name']})\n\n"
if self.channels:
msg += f"🎚️ Canali: {', '.join(map(str, self.channels))}\n"
if self.channel_labels:
msg += f"🎚️ Canali: {', '.join(self.channel_labels)}\n"
if self.mixes:
msg += f"🔊 Mix: {', '.join(map(str, self.mixes))}\n"
if self.mix_labels:
msg += f"🔊 Mix: {', '.join(self.mix_labels)}\n"
return msg
class ProfileManager:
"""Gestisce i profili utente."""
@@ -221,7 +190,6 @@ class ProfileManager:
return profiles
class ConversationManager:
"""Gestisce il salvataggio delle conversazioni su file JSON."""
@@ -289,7 +257,6 @@ class ConversationManager:
print(f"❌ Errore nell'eliminazione conversazione {user_id}: {e}")
return False
class TelegramBot:
"""Bot Telegram base con long polling."""
@@ -423,6 +390,7 @@ class TF5TelegramBot:
self.controller.set_channel_to_mix_level,
self.controller.set_channel_to_mix_on_off,
self.controller.get_channel_to_mix_info,
self.controller.get_full_mix_details,
],
temperature=0,
)
@@ -504,6 +472,37 @@ GRUPPI DI CANALI:
- "spegni tutto tranne..." → muta tutti gli altri
- "solo i microfoni" → attiva solo quelli, spegni il resto
---
ANALISI DEL MIX (per utenti esperti)
Quando un utente chiede di "analizzare un mix", "controllare i volumi della diretta", o "dare suggerimenti per il mix X", devi usare la funzione `get_full_mix_details(mix_number)`.
Questa funzione ti darà la lista di tutti i canali inviati a quel mix, con i loro livelli.
Il tuo compito è analizzare questi dati e fornire suggerimenti pratici per migliorare l'equilibrio, specialmente per un mix destinato a una diretta streaming o registrazione.
Linee guida per l'analisi:
1. **Gerarchia dei Volumi**:
- **Predicatore/Oratore Principale**: Deve essere il canale più alto di tutti. Cerca nomi come "Predicatore", "Pastore", "Pulpito". Il suo livello deve essere il punto di riferimento.
- **Voce Solista (Lead Vocal)**: Subito dopo il predicatore. Deve essere chiaramente sopra gli strumenti e i cori. Cerca "Voce L", "Cantante P".
- **Cori/Voci Secondarie**: Dovrebbero essere a un livello inferiore rispetto alla voce solista, per supportarla senza coprirla.
- **Strumenti Melodici**: (Tastiere, Chitarre) Dovrebbero creare un "tappeto sonoro" sotto le voci. Il loro livello deve essere bilanciato per non competere con le voci.
- **Sezione Ritmica**: (Basso, Batteria) Devono essere presenti ma controllati. Il basso deve essere udibile ma non rimbombante, la batteria presente ma non invadente.
2. **Problemi Comuni da Segnalare**:
- **Canali Spenti ma Inviati**: Controlla il campo `channel_is_on`. Se è `False` per un canale con un livello di send significativo, è un errore comune. Segnalalo dicendo: "Attenzione, il canale X ('Nome Canale') è spento sul suo fader principale, ma stai cercando di mandarlo al mix. Non si sentirà nulla. Devi prima accenderlo."
- **Livelli Estremi**: Segnala i canali con send molto alti (sopra +5 dB, rischio di distorsione) o molto bassi (sotto -30 dB, forse inutili o dimenticati).
- **Equilibrio Voci/Strumenti**: Se il livello medio degli strumenti è uguale o superiore a quello della voce solista, suggerisci di abbassare gli strumenti per fare spazio alla voce. Esempio: "Le tastiere sono a -5dB e la voce a -6dB. Prova ad abbassare un po' le tastiere per far emergere meglio la voce."
3. **Formato della Risposta**:
- Inizia con un riepilogo generale del mix (es: "Ecco un'analisi del Mix 15 (Diretta Streaming):").
- Usa sezioni chiare con emoji per una lettura rapida:
- `✅ Punti di Forza`: Se vedi un buon equilibrio di base.
- `⚠️ Suggerimenti per Migliorare`: Elenco puntato con azioni concrete.
- `🤔 Problemi Rilevati`: Per i canali spenti o livelli anomali.
- Sii propositivo e offri soluzioni, non solo problemi.
Esempio di analisi: "Ho analizzato il mix 15. La voce del predicatore è correttamente il canale più forte. Tuttavia, la chitarra acustica è quasi allo stesso livello della voce solista. Ti suggerisco di abbassare la chitarra di circa 3dB per dare più risalto al cantante. Ho notato anche che il canale 12 ('Coro Dx') è spento, quindi non sta contribuendo al mix. Vuoi che lo accenda?"
---
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)
@@ -536,13 +535,17 @@ Devi essere veloce, chiaro e capire anche richieste approssimative.
instruction += f"Nome: {profile.display_name}\n"
instruction += f"Ruolo: {role_info['name']} {role_info['emoji']}\n"
if profile.channels:
instruction += f"\nCanali assegnati: {', '.join(map(str, profile.channels))}\n"
instruction += "Quando l'utente dice 'il mio canale' o 'il mio mic/strumento', si riferisce a questi.\n"
if profile.channel_labels:
labels_str = ', '.join([f"'{l}'" for l in profile.channel_labels])
instruction += f"\nNomi dei canali assegnati: {labels_str}\n"
instruction += "Quando l'utente dice 'il mio canale' o 'il mio mic/strumento', si riferisce a questi. "
instruction += "**DEVI usare `search_channels_by_name` con questi nomi per trovare il numero di canale corretto prima di eseguire qualsiasi altra azione.**\n"
if profile.mixes:
instruction += f"\nMix assegnati: {', '.join(map(str, profile.mixes))}\n"
instruction += "Quando l'utente dice 'le mie cuffie', 'il mio monitor', 'in cuffia', si riferisce a questi mix.\n"
if profile.mix_labels:
labels_str = ', '.join([f"'{l}'" for l in profile.mix_labels])
instruction += f"\nNomi dei mix assegnati: {labels_str}\n"
instruction += "Quando l'utente dice 'le mie cuffie', 'il mio monitor', 'in cuffia', si riferisce a questi mix. "
instruction += "**DEVI usare `search_mixes_by_name` con questi nomi per trovare il numero di mix corretto prima di eseguire qualsiasi altra azione.**\n"
# Istruzioni specifiche per ruolo
permissions = role_info.get("permissions", [])
@@ -555,77 +558,27 @@ Devi essere veloce, chiaro e capire anche richieste approssimative.
else:
instruction += f"\n⚠️ LIMITAZIONI DI ACCESSO ({role_info['name']}):\n"
if "own_channel" in permissions and profile.channels:
instruction += f"- Canali controllabili: SOLO {', '.join(map(str, profile.channels))}\n"
if "own_channel" in permissions and profile.channel_labels:
instruction += f"- Canali controllabili: SOLO quelli che corrispondono ai nomi nel tuo profilo.\n"
if "own_mix" in permissions and profile.mixes:
instruction += f"- Mix controllabili: SOLO {', '.join(map(str, profile.mixes))}\n"
instruction += f"- Puoi gestire i send di QUALSIASI canale verso i TUOI mix ({', '.join(map(str, profile.mixes))})\n"
if "own_mix" in permissions and profile.mix_labels:
instruction += f"- Mix controllabili: SOLO quelli che corrispondono ai nomi nel tuo profilo.\n"
instruction += f"- Puoi gestire i send di QUALSIASI canale verso i TUOI mix.\n"
instruction += "\n❌ NON puoi:\n"
if "own_channel" not in permissions:
instruction += "- Modificare il volume/stato dei canali\n"
if "own_mix" not in permissions:
instruction += "- Modificare il volume/stato dei mix\n"
instruction += "- Caricare scene (solo i mixeristi)\n"
instruction += "- Accedere a canali/mix non assegnati\n"
instruction += "- Caricare scene (solo i mixeristi).\n"
instruction += "- Accedere a canali/mix non associati al tuo profilo.\n"
instruction += "\n💡 INTERPRETAZIONE RICHIESTE:\n"
instruction += "- 'alzami la chitarra' = alza il send della chitarra nel TUO mix\n"
instruction += "- 'più voce' = alza il send della voce nel TUO mix\n"
instruction += "- 'meno batteria in cuffia' = abbassa il send della batteria nel TUO mix\n"
instruction += "- Se l'utente chiede di fare qualcosa fuori dai suoi permessi, spiega gentilmente che non può e suggerisci di chiedere al mixerista\n"
instruction += "- 'alzami la chitarra' = alza il send della chitarra nel TUO mix (identificato dal suo nome).\n"
instruction += "- 'più voce' = alza il send della voce nel TUO mix.\n"
instruction += "- 'meno batteria in cuffia' = abbassa il send della batteria nel TUO mix.\n"
instruction += "- Se l'utente chiede di fare qualcosa fuori dai suoi permessi, spiega gentilmente che non può e suggerisci di chiedere al mixerista.\n"
instruction += f"\n{'='*70}\n"
return instruction
def _check_permission(self, profile: UserProfile, operation: str,
channel: Optional[int] = None,
mix: Optional[int] = None) -> tuple[bool, str]:
"""
Verifica se l'utente ha i permessi per un'operazione.
Ritorna (permitted, error_message)
"""
if not profile.setup_completed:
return False, "⚠️ Devi prima configurare il tuo profilo con /setup"
role_info = profile.get_role_info()
permissions = role_info.get("permissions", [])
# Mixeristi possono fare tutto
if "all" in permissions:
return True, ""
# Verifica permessi specifici
if operation in ["set_channel_level", "set_channel_on_off", "set_channel_pan",
"mute_multiple_channels", "unmute_multiple_channels"]:
if "own_channel" not in permissions:
return False, "❌ Solo i mixeristi possono controllare i canali direttamente"
if channel and not profile.can_access_channel(channel):
return False, f"❌ Non hai accesso al canale {channel}. I tuoi canali: {', '.join(map(str, profile.channels))}"
if operation in ["set_mix_level", "set_mix_on_off"]:
if "own_mix" not in permissions:
return False, "❌ Non hai permessi per controllare i mix"
if mix and not profile.can_access_mix(mix):
return False, f"❌ Non hai accesso al mix {mix}. I tuoi mix: {', '.join(map(str, profile.mixes))}"
if operation == "recall_scene":
return False, "❌ Solo i mixeristi possono caricare scene"
# Le operazioni di send ai mix sono permesse se hai accesso al mix di destinazione
if operation in ["set_channel_to_mix_level", "set_channel_to_mix_on_off"]:
if "own_mix" not in permissions:
return False, "❌ Non hai permessi per controllare i send ai mix"
if mix and not profile.can_access_mix(mix):
return False, f"❌ Non hai accesso al mix {mix}. I tuoi mix: {', '.join(map(str, profile.mixes))}"
return True, ""
def get_profile(self, user_id: int, username: str) -> UserProfile:
"""Ottiene il profilo di un utente."""
if user_id not in self.profiles:
@@ -694,7 +647,6 @@ Devi essere veloce, chiaro e capire anche richieste approssimative.
role_info = profile.get_role_info()
permissions = role_info.get("permissions", [])
# Se è mixerista, ha finito
if "all" in permissions:
profile.setup_completed = True
self.profile_manager.save_profile(profile)
@@ -708,59 +660,47 @@ Devi essere veloce, chiaro e capire anche richieste approssimative.
f"'Richiama la scena A10'\n"
f"'Mostrami tutti i canali'")
# Altrimenti, chiedi i canali (se necessario)
if "own_channel" in permissions:
self.bot.send_message(
user_id,
f"🎚️ Quali canali usi?\n\n"
f"Esempi:\n"
f"• Un canale: 5\n"
f"• Più canali: 5, 7, 12\n"
f"• Range: 5-8 (canali 5, 6, 7, 8)\n"
f"• Misti: 3, 5-7, 10\n\n"
f"(Scrivi 0 se non usi canali diretti)"
f"🎚️ **Come si chiama il tuo canale/strumento?**\n\n"
f"Scrivi il nome esatto che vedi sull'etichetta del mixer (es: 'VOX Marco', 'CH Acustica').\n"
f"Se hai più canali, separali con una virgola (es: 'Tastiera L', 'Tastiera R').\n\n"
f"(Scrivi 'nessuno' se non hai un canale specifico)"
)
self.setup_states[user_id] = {"step": "channels", "role": profile.role}
return ""
else:
# Salta direttamente ai mix
self.setup_states[user_id] = {"step": "channels"}
elif "own_mix" in permissions:
self.bot.send_message(
user_id,
f"🔊 Quali mix/aux usi per le tue cuffie o uscita?\n\n"
f"Esempi:\n"
f"• Un mix: 3\n"
f"• Più mix: 3, 5\n"
f"• Range: 3-5\n"
f"• Mix stereo: 9, 10 (in-ear stereo)\n\n"
f"(Scrivi 0 se non usi mix specifici)"
f"🔊 **Come si chiama il tuo mix per le cuffie/monitor?**\n\n"
f"Scrivi il nome esatto (es: 'InEar Marco', 'Cuffia Palco Sx').\n"
f"Se usi un mix stereo, scrivi entrambi i nomi separati da virgola (es: 'IEM L', 'IEM R').\n\n"
f"(Scrivi 'nessuno' se non usi un mix specifico)"
)
self.setup_states[user_id] = {"step": "mixes", "role": profile.role}
return ""
self.setup_states[user_id] = {"step": "mixes"}
return ""
# Step: inserimento canali
# Step: inserimento etichette canali
elif step == "channels":
channels = self._parse_channel_list(text)
profile.channels = channels
labels_text = text.strip()
if labels_text.lower() != 'nessuno':
profile.channel_labels = [label.strip() for label in labels_text.split(',')]
role_info = profile.get_role_info()
permissions = role_info.get("permissions", [])
# Chiedi i mix se necessario
if "own_mix" in permissions:
self.bot.send_message(
user_id,
f"🔊 Quali mix/aux usi per le tue cuffie o uscita?\n\n"
f"Esempi:\n"
f"• Un mix: 3\n"
f"• Più mix: 3, 5\n"
f"• Range: 3-5\n"
f"• Mix stereo: 9, 10 (in-ear stereo)\n\n"
f"(Scrivi 0 se non usi mix specifici)"
f"🔊 **Come si chiama il tuo mix per le cuffie/monitor?**\n\n"
f"Scrivi il nome esatto (es: 'InEar Marco', 'Cuffia Palco Sx').\n"
f"Se usi un mix stereo, scrivi entrambi i nomi separati da virgola (es: 'IEM L', 'IEM R').\n\n"
f"(Scrivi 'nessuno' se non usi un mix specifico)"
)
self.setup_states[user_id] = {"step": "mixes", "role": profile.role}
self.setup_states[user_id] = {"step": "mixes"}
return ""
else:
# Finito
profile.setup_completed = True
self.profile_manager.save_profile(profile)
del self.setup_states[user_id]
@@ -771,59 +711,32 @@ Devi essere veloce, chiaro e capire anche richieste approssimative.
f"'Alza il mio canale'\n"
f"'Spegni il mio mic'")
# Step: inserimento mix
# Step: inserimento etichette mix
elif step == "mixes":
mixes = self._parse_channel_list(text) # Stessa logica per i mix
profile.mixes = mixes
labels_text = text.strip()
if labels_text.lower() != 'nessuno':
profile.mix_labels = [label.strip() for label in labels_text.split(',')]
profile.setup_completed = True
self.profile_manager.save_profile(profile)
del self.setup_states[user_id]
role_info = profile.get_role_info()
example_commands = []
if profile.channels:
example_commands.append("'Alza il mio canale'")
if profile.mixes:
if profile.channel_labels:
example_commands.append("'Alza il volume del mio canale'")
if profile.mix_labels:
example_commands.extend([
"'Alzami la chitarra' (nel tuo mix)",
"'Alzami la chitarra nel mio monitor'",
"'Più voce in cuffia'",
"'Meno batteria'"
])
return (f"✅ Configurazione completata!\n\n"
f"{profile.get_summary()}\n"
f"Esempi di comandi:\n" + "\n".join(example_commands))
f"{profile.get_summary()}\n\n"
f"Esempi di comandi che puoi usare:\n" + "\n".join(example_commands))
return ""
def _parse_channel_list(self, text: str) -> List[int]:
"""Parse una lista di canali/mix dal testo utente."""
channels = []
# Rimuovi spazi e split per virgola
parts = [p.strip() for p in text.split(',')]
for part in parts:
# Range (es: 5-8)
if '-' in part:
try:
start, end = map(int, part.split('-'))
channels.extend(range(start, end + 1))
except:
pass
# Singolo numero
else:
try:
num = int(part)
if num > 0: # Ignora 0
channels.append(num)
except:
pass
return sorted(list(set(channels))) # Rimuovi duplicati e ordina
def _get_file_path(self, file_id: str) -> str:
"""Richiede a Telegram il percorso di un file."""
result = self.bot._make_request("getFile", {"file_id": file_id})
@@ -971,16 +884,16 @@ Devi essere veloce, chiaro e capire anche richieste approssimative.
msg += "'Alza il canale 5 a -10 dB'\n"
msg += "'Spegni i canali dal 1 al 5'\n"
msg += "'Richiama la scena A10'\n"
msg += "'Mostrami lo stato del canale 12'\n"
msg += "'Analizza il mix 15 per la diretta'\n"
else:
msg += "**Esempi di comandi:**\n"
if profile.channels:
if profile.channel_labels:
msg += "'Alza il mio canale'\n"
msg += "'Spegni il mio mic'\n"
if profile.mixes:
if profile.mix_labels:
msg += "'Alzami la chitarra' (nel tuo mix)\n"
msg += "'Più voce in cuffia'\n"
msg += "'Meno batteria'\n"
msg += "'Analizza il mio mix per la diretta'\n"
msg += "\n**Comandi speciali:**\n"
msg += "/help - Mostra questo messaggio\n"
@@ -1170,7 +1083,7 @@ def main():
try:
bot = TF5TelegramBot(
telegram_token=telegram_token,
mixer_host=args.mixer_host,
mixer_host=get_host(),
mixer_port=args.mixer_port,
conversations_dir=args.conversations_dir,
profiles_dir=args.profiles_dir