def _get_cache_file_path(self, text: str, use_cache: bool) -> str: """ Return file path to where the synthesized audio file will be saved / cached, if cache is enabled, otherwise return random file path. """ # TODO: Add cron job which auto purges old recording cached files file_hash = hashlib.md5( text.encode("utf-8") + self.implementation_id.encode("utf-8")).hexdigest() file_name = "%s%s" % (file_hash, self.file_extension) cache_generated_audio_files = get_config_option("tts", "enable_cache", "bool", fallback=False) if cache_generated_audio_files: cached_audio_files_path = get_config_option( "tts", "cache_directory") file_path = os.path.join(cached_audio_files_path, file_name) else: file_path = os.path.join("/tmp", file_name) if self._is_valid_cached_file(file_path=file_path, use_cache=use_cache): return file_path return file_path
def test_run_success_sequence_1(self): set_config_option("tts", "implementation", "invalid", write_to_disk=True) self.assertEqual(get_config_option("tts", "implementation", "str"), "invalid") self.assertConfigFileContainsValue(self._config_path, "tts", "implementation", "invalid") plugin = ChangeTTSImplementationAdminPluginForTest() self.assertEqual(len(plugin.mock_said_text), 0) plugin.initialize(config={}) plugin.run(sequence="1") self.assertEqual(get_config_option("tts", "implementation", "str"), "gtts") self.assertConfigFileContainsValue(self._config_path, "tts", "implementation", "gtts") expected_text = "TTS mode changed to online." self.assertEqual(len(plugin.mock_said_text), 1) self.assertEqual(plugin.mock_said_text[0], expected_text)
def __init__( self, implementation: str = "gtts", **implementation_kwargs: Any, ): cache_generated_audio_files = get_config_option("tts", "enable_cache", "bool", fallback=False) cached_audio_files_path = get_config_option("tts", "cache_directory", "str", fallback=None) if cache_generated_audio_files: os.makedirs(cached_audio_files_path, exist_ok=True) self._implementation = implementation self._implementation_kwargs = implementation_kwargs if implementation not in self.implementations: raise ValueError( "Invalid implementation: %s. Valid implementation are: %s" % (implementation, ",".join(self.implementations))) self._tts = self.implementations[implementation]() # type: ignore
def __init__(self): self._started = False self._all_plugins: Dict[str, BasePlugin] = {} self._dtmf_plugins: Dict[str, BaseDTMFPlugin] = {} self._sequence_to_plugin_map: Dict[str, BaseDTMFPlugin] = {} self._emulator_mode = False self._dev_mode = False self._offline_mode = False self._dtmf_decoder = DTMFDecoder() self._rx = RX( input_device_index=get_config_option("audio", "input_device_index", "int"), rate=get_config_option("audio", "sample_rate", "int"), ) self._plugin_executor = PluginExecutor( implementation=get_config_option("plugins", "executor")) self._scheduler = BackgroundScheduler() self._cron_jobs_to_run_lock = threading.Lock() # Holds a list of ids for cron jobs which should run during the next iteration of the main # loop self._cron_jobs_to_run: List[str] = []
def __init__(self): self._callsign = get_config_option("tx", "callsign") self._tx_mode = get_config_option("tx", "mode") self._gpio_pin = get_config_option("tx", "gpio_pin", fallback=None) self._audio_player = AudioPlayer() self._config = {}
def _get_text_format_context(self) -> Dict[str, str]: """ Return a string format context with which each cron job test is rendered with. This allows user to reference various dynamic values in this text such as current time, date, etc. """ station_id = self._config.get("weather_station_id", "default") now_dt = datetime.datetime.utcnow() observation_pb = get_weather_observation_for_date( station_id=station_id, date=now_dt) context = {} context["callsign"] = get_config_option("tx", "callsign", "str", fallback="unknown") context["day_of_week"] = now_dt.strftime("%A") context["date"] = now_dt.strftime("%Y-%m-%d") context["time_utc"] = datetime.datetime.utcnow().strftime("%H:%M") context["time_local"] = datetime.datetime.now().strftime("%H:%M") if observation_pb: context["weather_data"] = MessageToDict(observation_pb) else: context["weather_data"] = {} return context
def test_get_config_option(self): os.environ["RADIO_BRIDGE_CONFIG_PATH"] = CONFIG_PATH_1 value = get_config_option("tx", "callsign", "str") self.assertEqual(value, "ABCD") value = get_config_option("tx", "invalid", "str", fallback="default") self.assertEqual(value, "default") value = get_config_option("invalid_section", "callsign", "str", fallback="default1") self.assertEqual(value, "default1") # fallback not provided expected_msg = "Section invalid_section is empty or missing" self.assertRaisesRegex( ValueError, expected_msg, get_config_option, "invalid_section", "callsign", "str" )
def write_otps_to_disk(otps: List[str]) -> bool: """ Write provided OTPs to a local db file on disk, overwriting any existing content. """ otps_file_path = get_config_option("plugins", "admin_otps_file_path") with open(otps_file_path, "w") as fp: fp.write("\n".join(sorted(list(set(otps))))) return True
def _is_valid_cached_file(self, file_path: str, use_cache: bool = True): cache_generated_audio_files = get_config_option("tts", "enable_cache", "bool", fallback=False) if not cache_generated_audio_files or not use_cache: return False return os.path.isfile(file_path) and os.stat(file_path).st_size > 0
def _parse_and_validate_config(self, config) -> Dict[str, CronSayItemConfig]: result = {} for job_id, job_specs in config.items(): split = job_specs.split(JOB_SPEC_DELIMITER) if len(split) != 4: raise ValueError('Plugin job specification "%s" is invalid' % (job_specs)) job_trigger = split[0] job_kwargs_str = split[1] job_type = split[2] job_value = split[3] if job_type not in ["text", "text_to_morse", "morse", "file"]: raise ValueError('Unknown job type "%s" for job %s' % (job_type, job_id)) trigger_instance = self._get_job_trigger_for_job_spec( job_id, job_trigger, job_kwargs_str) item = CronSayItemConfig(job_id=job_id, trigger_instance=trigger_instance, type=job_type, value=job_value) trigger_interval_seconds = self._get_interval_in_seconds( item.trigger_instance) dev_mode = get_config_option("main", "dev_mode", "bool", fallback=False) if (not dev_mode and trigger_interval_seconds and trigger_interval_seconds < MINIMUM_TRIGGER_INTERVAL): raise ValueError( "Requested interval for job %s is %s seconds, but minimum " "allowed value is %s seconds" % (job_id, trigger_interval_seconds, MINIMUM_TRIGGER_INTERVAL)) if not dev_mode and item.duration > MAXIMUM_PLAYBACK_DURATION: raise ValueError( "Calculated audio duration for job %s is longer than maximum " "allowed (%s seconds > %s seconds)" % (job_id, item.duration, MAXIMUM_PLAYBACK_DURATION)) result[job_id] = item return result
def get_valid_otps() -> List[str]: """ Return a list of all the OTPs which are still valid (unused) from a local db file on disk. """ otps_file_path = get_config_option("plugins", "admin_otps_file_path") valid_otps = [] if otps_file_path and os.path.isfile(otps_file_path): with open(otps_file_path, "r") as fp: content = fp.read().strip() if content: valid_otps = content.splitlines() return sorted(valid_otps)
def __init__(self): self._max_run_time = get_config_option("plugins", "max_run_time", "int", fallback=None)
def initialize( self, dev_mode: bool = False, emulator_mode: bool = False, offline_mode: bool = False, debug: bool = False, ): # 1. Configure logging configure_logging(get_config_option("main", "logging_config"), debug=debug) wx_server_load_and_parse_config(WX_SERVER_CONFIG_PATH) if dev_mode: LOG.info("Development mode is active") set_config_option("main", "dev_mode", "True") if emulator_mode: LOG.info("Running in emulator mode") set_config_option("main", "emulator_mode", "True") if offline_mode: LOG.info( "Running in offline mode. Only plugins which don't require internet " 'connection will be available and using "espeak" TTS engine.') set_config_option("main", "offline_mode", "True") set_config_option("tts", "implementation", "espeak") self._emulator_mode = get_config_option("main", "emulator_mode", "bool", fallback=False) self._dev_mode = get_config_option("main", "dev_mode", "bool", fallback=False) self._offline_mode = get_config_option("main", "offline_mode", "bool", fallback=False) # 2. Load and register plugins self._all_plugins = get_available_plugins() self._dtmf_plugins = get_plugins_with_dtmf_sequence() self._sequence_to_plugin_map = self._dtmf_plugins # Filter out plugins in offline mode # TODO: Move that functionality to get_ methods if self._offline_mode: self._all_plugins = { name: instance for (name, instance) in self._all_plugins.items() if instance.REQUIRES_INTERNET_CONNECTION is False } self._dtmf_plugins = { name: instance for (name, instance) in self._dtmf_plugins.items() if instance.REQUIRES_INTERNET_CONNECTION is False } # 3. Generate OTPs all_otps, _ = generate_and_write_otps() LOG.info("Generated and unused OTPs for admin commands", otps=all_otps)
def _tts(self): # NOTE: We instantiate this object lazily on demand so any changes to the config state made # during the program life cycle are reflected here. return TextToSpeech(implementation=get_config_option("tts", "implementation"))