#!/usr/bin/env python3 # piper-worker — sintetizador de voz aislado para RadioCheck. # # Copyright (C) 2026 Rubén Martínez López # # Este programa es software libre: puedes redistribuirlo y/o modificarlo bajo los # términos de la Licencia Pública General GNU publicada por la Free Software # Foundation, en su versión 3 o cualquier versión posterior. # # Se distribuye con la esperanza de que sea útil, pero SIN NINGUNA GARANTÍA; ni # siquiera la garantía implícita de COMERCIALIZACIÓN o APTITUD PARA UN PROPÓSITO # PARTICULAR. Consulta la Licencia Pública General GNU para más detalles. # Deberías haber recibido una copia junto a este programa; si no, ve a # . # # ───────────────────────────────────────────────────────────────────────────── # POR QUÉ ESTE PROGRAMA EXISTE POR SEPARADO # # El motor de síntesis (piper-tts) es GPL-3.0-or-later y lleva espeak-ng # compilado dentro. La GPL exige que todo programa que ENLACE ese código sea # también GPL. RadioCheck es una aplicación cerrada, así que no puede importarlo. # # La solución es la misma que usa cualquier aplicación que trae ffmpeg dentro: # el código GPL vive en un programa APARTE —éste—, que se comunica con la # aplicación a distancia, por su entrada y su salida estándar. Este programa es # GPL y su código fuente se publica; RadioCheck sólo lo invoca. # # Este programa hace UNA cosa: recibir texto y devolver audio crudo. Todo el # procesado de sonido de RadioCheck (el filtro de radio, los pitidos, la caché) # ocurre del otro lado y no forma parte de esto. # # PROTOCOLO (una petición por línea, por la entrada estándar): # {"text": "...", "length_scale": 0.65, "noise_scale": 0.9, "speaker_id": null} # Respuesta por la salida estándar: una línea JSON con la cabecera y, si todo fue # bien, inmediatamente después los bytes de audio PCM 16 bits: # {"ok": true, "bytes": 12345, "rate": 22050}\n<12345 bytes> # {"ok": false, "error": "..."}\n # La entrada estándar se cierra → el programa termina. # # Uso: piper-worker import json import sys def main() -> int: if len(sys.argv) < 2: sys.stderr.write("uso: piper-worker \n") return 2 try: from piper import PiperVoice, SynthesisConfig except ImportError as e: sys.stderr.write(f"piper-tts no disponible: {e}\n") return 3 try: voz = PiperVoice.load(sys.argv[1]) except Exception as e: sys.stderr.write(f"no se pudo cargar el modelo: {e}\n") return 4 rate = voz.config.sample_rate salida = sys.stdout.buffer # Aviso de que estamos listos: la aplicación espera esta línea antes de pedir # nada, para no confundir un arranque lento con un fallo. salida.write(json.dumps({"ok": True, "ready": True, "rate": rate}).encode() + b"\n") salida.flush() for linea in sys.stdin.buffer: linea = linea.strip() if not linea: continue try: pet = json.loads(linea) cfg = SynthesisConfig( length_scale=float(pet.get("length_scale", 1.0)), noise_scale=float(pet.get("noise_scale", 0.667)), speaker_id=pet.get("speaker_id"), ) crudo = b"".join(t.audio_int16_bytes for t in voz.synthesize(pet["text"], syn_config=cfg)) salida.write(json.dumps({"ok": True, "bytes": len(crudo), "rate": rate}).encode() + b"\n") salida.write(crudo) salida.flush() except Exception as e: salida.write(json.dumps({"ok": False, "error": str(e)}).encode() + b"\n") salida.flush() return 0 if __name__ == "__main__": raise SystemExit(main())