diff --git a/vps_opencode_bot/aviso_voz.py b/vps_opencode_bot/aviso_voz.py new file mode 100644 index 0000000..0cfdadf --- /dev/null +++ b/vps_opencode_bot/aviso_voz.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +""" +aviso_voz.py — Genera y envia una nota de voz corta por Telegram al terminar +una respuesta de OpenCode, en vez del pitido/tono de notificacion. + +Usa edge-tts (gratis/ilimitado, Microsoft) con la voz preferida del usuario +es-MX-DaliaNeural. Telegram exige las notas de voz en OGG/Opus, asi que el +mp3 generado por edge-tts se convierte con ffmpeg la primera vez y se cachea. + +Configuracion (variables del .env del bot): + AVISO_VOZ=1 -> habilita el aviso de voz al terminar + AVISO_VOZ_FRASE=Listo. -> frase a sintetizar (opcional) + AVISO_VOZ_MP3= -> usar un mp3 propio (voz ElevenLabs) + en vez de sintetizar (opcional) +El OGG cacheado vive en /sounds/aviso_fin.ogg (o junto al mp3 indicado). + +Uso: + python3 aviso_voz.py -> envia la nota de voz al chat +""" +import asyncio +import os +import subprocess +import sys + +import requests + +BASE = os.path.dirname(os.path.abspath(__file__)) +SND = os.path.join(BASE, "sounds") +ENV = {} +try: + for linea in open(os.path.join(BASE, ".env"), encoding="utf-8"): + linea = linea.strip() + if linea and not linea.startswith("#") and "=" in linea: + k, v = linea.split("=", 1) + ENV[k.strip()] = v.strip() +except Exception: + pass + +TOKEN = ENV.get("TELEGRAM_BOT_TOKEN", "") +API = f"https://api.telegram.org/bot{TOKEN}" +FRASE = ENV.get("AVISO_VOZ_FRASE", "Listo.") +MP3_SOURCE = ENV.get("AVISO_VOZ_MP3", "") + + +def _voz_dalia(texto, salida_mp3): + """Sintetiza `texto` con edge-tts voz es-MX-DaliaNeural a un mp3.""" + import edge_tts + asyncio.run(edge_tts.Communicate( + texto, "es-MX-DaliaNeural", rate="-8%").save(salida_mp3)) + + +def asegurar_ogg(): + """Devuelve la ruta del OGG/Opus de aviso, generandolo/cacheandolo.""" + try: + os.makedirs(SND, exist_ok=True) + except Exception: + pass + # Si hay mp3 propio de voz, el cache es su mismo nombre con .ogg + if MP3_SOURCE and os.path.isfile(MP3_SOURCE): + ogg = os.path.splitext(MP3_SOURCE)[0] + ".ogg" + fuente = MP3_SOURCE + else: + ogg = os.path.join(SND, "aviso_fin.ogg") + fuente = os.path.join(SND, "aviso_fin.mp3") + if not os.path.exists(fuente): + try: + _voz_dalia(FRASE, fuente) + except Exception as e: + print(f"[aviso_voz] edge-tts error: {e}") + return None + if not os.path.exists(ogg): + try: + r = subprocess.run( + ["ffmpeg", "-y", "-loglevel", "error", "-i", fuente, + "-c:a", "libopus", "-b:a", "48k", ogg], + capture_output=True, timeout=60) + if r.returncode != 0: + print(f"[aviso_voz] ffmpeg error: {r.stderr.decode(errors='replace')[:300]}") + return None + except Exception as e: + print(f"[aviso_voz] ffmpeg no disponible: {e}") + return None + return ogg + + +def enviar_voz(chat_id): + ogg = asegurar_ogg() + if not ogg: + return False + try: + with open(ogg, "rb") as f: + r = requests.post(f"{API}/sendVoice", + data={"chat_id": chat_id}, + files={"voice": ("aviso.ogg", f, + "audio/ogg")}, + timeout=40) + return r.json().get("ok", False) + except Exception as e: + print(f"[aviso_voz] sendVoice error: {e}") + return False + + +if __name__ == "__main__": + chat = sys.argv[1] if len(sys.argv) > 1 else "" + if not chat or not TOKEN: + print("Uso: python3 aviso_voz.py (con TELEGRAM_BOT_TOKEN en .env)") + sys.exit(1) + ok = enviar_voz(chat) + print(f"[aviso_voz] {'OK' if ok else 'FALLO'} -> chat {chat}") + sys.exit(0 if ok else 2) diff --git a/vps_opencode_bot/opencode_bot.py b/vps_opencode_bot/opencode_bot.py index 73436a0..951845b 100644 --- a/vps_opencode_bot/opencode_bot.py +++ b/vps_opencode_bot/opencode_bot.py @@ -160,6 +160,22 @@ def editar(chat_id, mid, texto, filas=None): tg("editMessageText", data) +def _aviso_voz_fin(chat): + """Aviso sonoro de fin de turno (nota de voz) si esta habilitado.""" + if ENV.get("AVISO_VOZ", "0") != "1": + return + try: + subprocess.run( + [sys.executable, os.path.join(BASE, "aviso_voz.py"), str(chat)], + timeout=90, capture_output=True) + except Exception as e: + log(f"aviso_voz error: {e}") + + +def aviso_voz_fin(chat): + threading.Thread(target=_aviso_voz_fin, args=(chat,), daemon=True).start() + + def typing(chat_id): """Manda sendChatAction 'typing' cada 4 s hasta que se detenga.""" stop = threading.Event() @@ -460,6 +476,9 @@ def procesar_texto(chat, texto): for ruta in adjuntos[:6]: enviar_doc(chat, ruta) + # Aviso sonoro de fin (nota de voz en el movil), en segundo plano. + aviso_voz_fin(chat) + def cambiar_repo(chat, slug): ruta = os.path.join(GH, slug)