06-sep: [feat] aviso de voz al terminar (Dalia/edge-tts) en opencode-tg + script aviso_voz
This commit is contained in:
110
vps_opencode_bot/aviso_voz.py
Normal file
110
vps_opencode_bot/aviso_voz.py
Normal 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)
|
||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user