06-sep: [feat] aviso de voz al terminar (Dalia/edge-tts) en opencode-tg + script aviso_voz

This commit is contained in:
juanjo
2026-09-06 20:17:32 +02:00
parent f6116d1af2
commit fd3f190534
2 changed files with 129 additions and 0 deletions

View File

@@ -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=<ruta> -> usar un mp3 propio (voz ElevenLabs)
en vez de sintetizar (opcional)
El OGG cacheado vive en <dir>/sounds/aviso_fin.ogg (o junto al mp3 indicado).
Uso:
python3 aviso_voz.py <chat_id> -> 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 <chat_id> (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)

View File

@@ -160,6 +160,22 @@ def editar(chat_id, mid, texto, filas=None):
tg("editMessageText", data) 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): def typing(chat_id):
"""Manda sendChatAction 'typing' cada 4 s hasta que se detenga.""" """Manda sendChatAction 'typing' cada 4 s hasta que se detenga."""
stop = threading.Event() stop = threading.Event()
@@ -460,6 +476,9 @@ def procesar_texto(chat, texto):
for ruta in adjuntos[:6]: for ruta in adjuntos[:6]:
enviar_doc(chat, ruta) 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): def cambiar_repo(chat, slug):
ruta = os.path.join(GH, slug) ruta = os.path.join(GH, slug)