diff --git a/.gitignore b/.gitignore index 2d1a2e4..9e3a0e9 100644 --- a/.gitignore +++ b/.gitignore @@ -34,7 +34,8 @@ json/* !json/.gitkeep docs/* !docs/PARROT.md -Audio/ +Audio/* +!Audio/.gitkeep # ----------------------------------------------------------------------------- # Python diff --git a/Audio/.gitkeep b/Audio/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md index 6554782..9c51d11 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,9 @@ Copy `adn-server.example.yaml` to `adn-server.yaml` and edit with your settings. Voice features (announcements, TTS, recording) use a separate config file. Copy `adn-voice.example.yaml` to `adn-voice.yaml` and edit. If the file does not exist, voice features are disabled (no error). Changes are hot-reloaded every 15 seconds. -TTS (Text-to-Speech) requires **ffmpeg** installed on the system (see Requirements above). The pipeline is: `.txt` → gTTS → `.mp3` → ffmpeg → `.wav` → vocoder → `.ambe` +- **Each item** (ANNOUNCEMENTS, TTS_ANNOUNCEMENTS) has its own `LANGUAGE` and `ENABLED: true` to activate. +- **ANNOUNCEMENT_LANGUAGES** is optional (for voice ident only); announcements/TTS work without it. +- **TTS** requires ffmpeg + vocoder (TTS_VOCODER_CMD or TTS_AMBESERVER_HOST). Pipeline: `.txt` → gTTS → `.mp3` → ffmpeg → `.wav` → vocoder → `.ambe` ## Run diff --git a/adn-voice.example.yaml b/adn-voice.example.yaml index 1b625f0..a38b988 100644 --- a/adn-voice.example.yaml +++ b/adn-voice.example.yaml @@ -3,10 +3,12 @@ # # If adn-voice.yaml does not exist, voice features are disabled (no error). # This file is re-checked every 15 s; changes take effect without restart. +# +# Each announcement/TTS has its own LANGUAGE. ENABLED: true activates that item. VOICE: - # Languages available for voice announcements (comma-separated, e.g. "es_ES,en_GB"). - # Leave empty to disable voice features. + # Optional: languages for voice ident (MASTER systems with VOICE_IDENT). + # If empty, ident uses per-system ANNOUNCEMENT_LANGUAGE. Not needed for announcements/TTS. ANNOUNCEMENT_LANGUAGES: "" # Base path for AMBE audio files (relative to project root). @@ -17,22 +19,23 @@ VOICE: # Pre-recorded .ambe files played on a talkgroup at intervals or hourly. # FILE: filename without .ambe, looked up in Audio//ondemand/ # MODE: "hourly" (once per hour) or "interval" (every INTERVAL seconds) + # ENABLED: true to activate; false to keep configured but inactive # --------------------------------------------------------------------------- ANNOUNCEMENTS: [] # Example (all variables used): # ANNOUNCEMENTS: - # - ENABLED: false + # - ENABLED: true # FILE: announcement # TG: 2 # MODE: hourly # INTERVAL: 3600 # LANGUAGE: en_GB - # - ENABLED: false + # - ENABLED: true # FILE: announcement2 # TG: 2 # MODE: interval - # INTERVAL: 1800 - # LANGUAGE: en_GB + # INTERVAL: 60 + # LANGUAGE: es_ES # --------------------------------------------------------------------------- # Recording @@ -50,6 +53,7 @@ VOICE: # TTS (Text-to-Speech) — array # Converts .txt files to AMBE via: gTTS -> ffmpeg -> vocoder/AMBEServer # FILE: filename without .txt, looked up in Audio//ondemand/ + # ENABLED: true to activate; requires TTS_VOCODER_CMD or TTS_AMBESERVER_HOST # --------------------------------------------------------------------------- # External vocoder command ({wav} and {ambe} are replaced at runtime). @@ -70,15 +74,15 @@ VOICE: # Example: # TTS_ANNOUNCEMENTS: - # - ENABLED: false + # - ENABLED: true # FILE: text1 # TG: 2 # MODE: hourly # INTERVAL: 3600 # LANGUAGE: en_GB - # - ENABLED: false + # - ENABLED: true # FILE: text2 # TG: 2 # MODE: interval - # INTERVAL: 3600 - # LANGUAGE: en_GB + # INTERVAL: 60 + # LANGUAGE: es_ES diff --git a/src/adn_server/application/ident_use_cases.py b/src/adn_server/application/ident_use_cases.py index 68118f2..645439b 100644 --- a/src/adn_server/application/ident_use_cases.py +++ b/src/adn_server/application/ident_use_cases.py @@ -65,6 +65,13 @@ class IdentUseCases: systems_cfg = self._config.get("SYSTEMS", {}) protocols = self._get_protocols() ann_lang = (self._config.get("VOICE", {}).get("ANNOUNCEMENT_LANGUAGES") or "").strip() + if not ann_lang: + # Fallback: derive from MASTER systems with VOICE_IDENT (each has ANNOUNCEMENT_LANGUAGE) + langs = set() + for sys_cfg in systems_cfg.values(): + if sys_cfg.get("MODE") == "MASTER" and sys_cfg.get("VOICE_IDENT"): + langs.add(sys_cfg.get("ANNOUNCEMENT_LANGUAGE", "en_GB")) + ann_lang = ",".join(sorted(langs)) if langs else "" if not ann_lang: return words_by_lang = self._voice.get_ambe_words(ann_lang, self._audio_path) diff --git a/src/adn_server/application/voice_use_cases.py b/src/adn_server/application/voice_use_cases.py index 9626eeb..da0329c 100644 --- a/src/adn_server/application/voice_use_cases.py +++ b/src/adn_server/application/voice_use_cases.py @@ -534,12 +534,9 @@ class VoiceUseCases: protocol = self._get_protocols().get(system) if not protocol or not getattr(protocol, "STATUS", None): return - ann_lang = (self._config.get("VOICE", {}).get("ANNOUNCEMENT_LANGUAGES") or "").strip() - if not ann_lang: - return - words_by_lang = self.get_ambe_words(ann_lang, self._audio_path) sys_cfg = self._config.get("SYSTEMS", {}).get(system, {}) _lang = sys_cfg.get("ANNOUNCEMENT_LANGUAGE", "en_GB") + words_by_lang = self.get_ambe_words(_lang, self._audio_path) if _lang not in words_by_lang: return words = words_by_lang[_lang] diff --git a/src/adn_server/main.py b/src/adn_server/main.py index df229ac..170b0c4 100644 --- a/src/adn_server/main.py +++ b/src/adn_server/main.py @@ -234,10 +234,12 @@ def main() -> None: recording_handler = RecordingHandler(config, project_root) - # Voice: AMBE words + pkt_gen (legacy readAMBE + mk_voice). Use Default when languages + audio path set. + # Voice: AMBE words + pkt_gen (legacy readAMBE + mk_voice). Use Default when Audio dir exists. + # ANNOUNCEMENT_LANGUAGES is only for voice ident; announcements/TTS use per-item LANGUAGE. audio_path = os.path.join(project_root, config.get("VOICE", {}).get("AUDIO_PATH", "Audio")) - ann_lang = (config.get("VOICE", {}).get("ANNOUNCEMENT_LANGUAGES") or "").strip() - if ann_lang and os.path.isdir(audio_path): + if config.get("VOICE") and not os.path.isdir(audio_path): + os.makedirs(audio_path, exist_ok=True) + if os.path.isdir(audio_path): voice_provider = DefaultVoiceProvider() else: voice_provider = StubVoiceProvider()