From e1c0bd233fdc76ac44fa7d89c07912f3ec87b9f5 Mon Sep 17 00:00:00 2001 From: Joaquin Madrid Belando Date: Wed, 4 Mar 2026 17:12:47 +0100 Subject: [PATCH] Integrar sistema TTS con scheduling y playback --- bridge_master.py | 191 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 189 insertions(+), 2 deletions(-) diff --git a/bridge_master.py b/bridge_master.py index b2942ab..34bb697 100644 --- a/bridge_master.py +++ b/bridge_master.py @@ -75,6 +75,7 @@ from utils import load_json, save_json #Read voices from read_ambe import readAMBE +from tts_engine import ensure_tts_ambe #Remap some words for certain languages from i8n_voice_map import voiceMap @@ -877,6 +878,9 @@ def ident(): _announcement_last_hour = {1: -1, 2: -1, 3: -1, 4: -1} _announcement_running = {1: False, 2: False, 3: False, 4: False} +_tts_last_hour = {1: -1, 2: -1, 3: -1, 4: -1} +_tts_running = {1: False, 2: False, 3: False, 4: False} + _FRAME_INTERVAL = 0.054 _RECORDING_MAX_FRAMES = 2750 @@ -1105,7 +1109,168 @@ def scheduledAnnouncement(_ann_num=1): logger.info('(%s) Broadcasting %s packets to %s systems simultaneously: %s', _label, len(_pkts), len(_targets), _sys_names) _announcement_running[_ann_num] = True reactor.callLater(1.0, _announcementSendBroadcast, _targets, _pkts, 0, _source_id, _dst_id, _tg, _timeslot, _ann_num) - + +def scheduledTTSAnnouncement(_tts_num=1): + global _tts_last_hour, _tts_running + + _prefix = 'TTS_ANNOUNCEMENT{}'.format(_tts_num) + _label = 'TTS-{}'.format(_tts_num) + + if not CONFIG['GLOBAL'].get('{}_ENABLED'.format(_prefix), False): + return + + if _tts_running[_tts_num]: + logger.debug('(%s) Previous TTS announcement still running, skipping', _label) + return + + _mode = CONFIG['GLOBAL']['{}_MODE'.format(_prefix)] + + if _mode == 'hourly': + _now = datetime.now() + if _now.minute != 0: + return + if _now.hour == _tts_last_hour[_tts_num]: + return + _tts_last_hour[_tts_num] = _now.hour + + _file = CONFIG['GLOBAL']['{}_FILE'.format(_prefix)] + _tg = CONFIG['GLOBAL']['{}_TG'.format(_prefix)] + _timeslot = CONFIG['GLOBAL']['{}_TIMESLOT'.format(_prefix)] + _lang = CONFIG['GLOBAL']['{}_LANGUAGE'.format(_prefix)] + _slot_index = 2 if _timeslot == 2 else 1 + _dst_id = bytes_3(_tg) + _source_id = bytes_3(5000) + _peer_id = CONFIG['GLOBAL']['SERVER_ID'] + + _ambe_path = ensure_tts_ambe(CONFIG, _tts_num) + + if not _ambe_path: + logger.warning('(%s) No AMBE file available for TTS announcement %s', _label, _file) + return + + logger.info('(%s) Playing TTS file: %s to TG %s TS%s (mode: %s, lang: %s)', _label, _file, _tg, _timeslot, _mode, _lang) + + _say = [] + try: + _relative_path = _ambe_path + if _relative_path.startswith('./Audio/'): + _relative_path = _relative_path[len('./Audio'):] + elif _relative_path.startswith('Audio/'): + _relative_path = '/' + _relative_path[len('Audio'):] + _say.append(AMBEobj.readSingleFile(_relative_path)) + except IOError: + logger.warning('(%s) Cannot read AMBE file: %s', _label, _ambe_path) + return + except Exception as e: + logger.error('(%s) Error reading AMBE file: %s', _label, e) + return + + logger.debug('(%s) AMBE file loaded, %s words', _label, len(_say)) + + _excluded = ['ECHO', 'D-APRS'] + _targets = [] + + for _sn in list(systems.keys()): + if _sn in _excluded or any(_sn.startswith(ex + '-') for ex in _excluded): + continue + if _sn not in CONFIG['SYSTEMS']: + continue + if CONFIG['SYSTEMS'][_sn]['MODE'] != 'MASTER': + continue + if 'PEERS' not in CONFIG['SYSTEMS'][_sn]: + continue + + _has_peers = False + try: + for _pid in CONFIG['SYSTEMS'][_sn]['PEERS']: + if CONFIG['SYSTEMS'][_sn]['PEERS'][_pid]['CALLSIGN']: + _has_peers = True + break + except (KeyError, TypeError, RuntimeError): + continue + + if not _has_peers: + continue + + if _sn not in systems: + continue + + _slot = systems[_sn].STATUS[_slot_index] + if (_slot['RX_TYPE'] != HBPF_SLT_VTERM) or (_slot['TX_TYPE'] != HBPF_SLT_VTERM): + logger.debug('(%s) System %s busy, skipping', _label, _sn) + continue + + _targets.append({ + 'sys_obj': systems[_sn], + 'name': _sn, + 'slot': _slot + }) + + if not _targets: + logger.info('(%s) No systems with connected peers to send to', _label) + return + + _pkts = list(pkt_gen(_source_id, _dst_id, _peer_id, _slot_index - 1, _say)) + _sys_names = ', '.join([t['name'] for t in _targets[:5]]) + if len(_targets) > 5: + _sys_names += ', ... +{}'.format(len(_targets) - 5) + logger.info('(%s) Broadcasting %s packets to %s systems simultaneously: %s', _label, len(_pkts), len(_targets), _sys_names) + _tts_running[_tts_num] = True + reactor.callLater(1.0, _ttsSendBroadcast, _targets, _pkts, 0, _source_id, _dst_id, _tg, _timeslot, _tts_num) + +def _ttsSendBroadcast(_targets, _pkts, _pkt_idx, _source_id, _dst_id, _tg, _ts, _tts_num=1, _next_time=None): + global _tts_running + + _label = 'TTS-{}'.format(_tts_num) + + if _pkt_idx >= len(_pkts): + for _t in _targets: + try: + for _sid in list(_t['sys_obj'].STATUS.keys()): + if _sid not in (1, 2): + del _t['sys_obj'].STATUS[_sid] + except: + pass + _tts_running[_tts_num] = False + logger.info('(%s) Broadcast complete: %s packets sent to %s systems', _label, len(_pkts), len(_targets)) + return + + _now = time() + pkt = _pkts[_pkt_idx] + _stream_id = pkt[16:20] + + for _t in _targets: + try: + _sys_obj = _t['sys_obj'] + _slot = _t['slot'] + if _stream_id not in _sys_obj.STATUS: + _sys_obj.STATUS[_stream_id] = { + 'START': _now, + 'CONTENTION':False, + 'RFS': _source_id, + 'TGID': _dst_id, + 'LAST': _now + } + _slot['TX_TGID'] = _dst_id + else: + _sys_obj.STATUS[_stream_id]['LAST'] = _now + _slot['TX_TIME'] = _now + _sys_obj.send_system(pkt) + except Exception as e: + logger.error('(%s) Error sending packet %s to %s: %s', _label, _pkt_idx, _t['name'], e) + + _elapsed = time() - _now + if _next_time is None: + _next_time = _now + _FRAME_INTERVAL + else: + _next_time = _next_time + _FRAME_INTERVAL + _delay = max(0.001, _next_time - time()) + + if _pkt_idx < 3: + logger.debug('(%s) Packet %s/%s broadcast to %s systems (proc: %.1fms, delay: %.1fms)', _label, _pkt_idx + 1, len(_pkts), len(_targets), _elapsed * 1000, _delay * 1000) + + reactor.callLater(_delay, _ttsSendBroadcast, _targets, _pkts, _pkt_idx + 1, _source_id, _dst_id, _tg, _ts, _tts_num, _next_time) + def bridge_reset(): logger.debug('(BRIDGERESET) Running bridge resetter') for _system in CONFIG['SYSTEMS']: @@ -3384,7 +3549,29 @@ if __name__ == '__main__': CONFIG['GLOBAL']['{}_LANGUAGE'.format(_prefix)]) if _ann_mode == 'interval': logger.info('(%s) Interval: every %s seconds', _label, _ann_check_interval) - + + for _tts_num in range(1, 5): + _prefix = 'TTS_ANNOUNCEMENT{}'.format(_tts_num) + _label = 'TTS-{}'.format(_tts_num) + if CONFIG['GLOBAL'].get('{}_ENABLED'.format(_prefix), False): + _tts_mode = CONFIG['GLOBAL']['{}_MODE'.format(_prefix)] + if _tts_mode == 'hourly': + _tts_check_interval = 30 + else: + _tts_check_interval = CONFIG['GLOBAL']['{}_INTERVAL'.format(_prefix)] + _tts_task = task.LoopingCall(scheduledTTSAnnouncement, _tts_num) + _tts_def = _tts_task.start(_tts_check_interval, now=False) + _tts_def.addErrback(loopingErrHandle) + logger.info('(%s) Scheduled TTS announcements enabled - mode: %s, file: %s, TG: %s, TS: %s, lang: %s', + _label, + _tts_mode, + CONFIG['GLOBAL']['{}_FILE'.format(_prefix)], + CONFIG['GLOBAL']['{}_TG'.format(_prefix)], + CONFIG['GLOBAL']['{}_TIMESLOT'.format(_prefix)], + CONFIG['GLOBAL']['{}_LANGUAGE'.format(_prefix)]) + if _tts_mode == 'interval': + logger.info('(%s) Interval: every %s seconds', _label, _tts_check_interval) + #Security downloads from central server init_security_downloads(CONFIG)