From d04f4f8fc6bedbd14e8ca81a7b6bfa64c294c389 Mon Sep 17 00:00:00 2001 From: swanie98635 Date: Wed, 4 Feb 2026 15:54:56 -0800 Subject: [PATCH] feat: add French language support and update config example --- asl3_wx_announce/audio.py | 10 +++- asl3_wx_announce/main.py | 4 +- asl3_wx_announce/narrator.py | 98 +++++++++++++++++++++++---------- asl3_wx_announce/provider/ec.py | 7 ++- config.yaml.example | 6 +- 5 files changed, 89 insertions(+), 36 deletions(-) diff --git a/asl3_wx_announce/audio.py b/asl3_wx_announce/audio.py index 8aa4dcd..6c2fd3d 100644 --- a/asl3_wx_announce/audio.py +++ b/asl3_wx_announce/audio.py @@ -19,8 +19,16 @@ class AudioHandler: # Simple cleanup? if os.path.exists(filepath): os.remove(filepath) + + # Resolve language code + lang_cfg = self.config.get('language', 'en') + lang_map = { + 'en': 'en-US', + 'fr': 'fr-FR' + } + lang_code = lang_map.get(lang_cfg, 'en-US') - cmd = self.tts_template.format(file=filepath, text=text) + cmd = self.tts_template.format(file=filepath, text=text, lang=lang_code) self.logger.info(f"Generating audio: {cmd}") try: diff --git a/asl3_wx_announce/main.py b/asl3_wx_announce/main.py index 1a6836d..61d96a3 100644 --- a/asl3_wx_announce/main.py +++ b/asl3_wx_announce/main.py @@ -37,7 +37,7 @@ def do_full_report(config): sun_info = astro.get_astro_info(loc_info) # Narrate - narrator = Narrator() + narrator = Narrator(config) text = narrator.build_full_report(loc_info, conditions, forecast, alerts, sun_info) logger.info(f"Report Text: {text}") @@ -54,7 +54,7 @@ def monitor_loop(config): known_alerts = set() loc_svc = LocationService(config) - narrator = Narrator() + narrator = Narrator(config) handler = AudioHandler(config) nodes = config.get('audio', {}).get('nodes', []) prov_code = config.get('location', {}).get('provider') diff --git a/asl3_wx_announce/narrator.py b/asl3_wx_announce/narrator.py index a50ab11..1307d1a 100644 --- a/asl3_wx_announce/narrator.py +++ b/asl3_wx_announce/narrator.py @@ -1,79 +1,117 @@ from datetime import datetime import pytz -from typing import List +from typing import List, Dict from .models import LocationInfo, CurrentConditions, WeatherForecast, WeatherAlert class Narrator: - def __init__(self): - pass + STRINGS = { + 'en': { + 'wind_fmt': ", with winds from the {dir} at {kph} kilometers per hour, or {mph} miles per hour", + 'current_intro': "Current conditions for {city}, {region}.", + 'temp_fmt': "The temperature is {temp_c} degrees celsius, {temp_f} degrees fahrenheit.", + 'conditions_fmt': "Conditions are {desc}{wind}.", + 'forecast_intro': "Here is the forecast. ", + 'high_fmt': " with a high of {c} celsius, {f} fahrenheit", + 'low_fmt': " with a low of {c} celsius, {f} fahrenheit", + 'period_fmt': "{period}: {summary}{temp}. ", + 'no_alerts': "There are no active weather alerts.", + 'alerts_intro': "There are {count} active weather alerts. ", + 'alert_item': "A {title} is in effect until {expires}. ", + 'greeting': "Good day. The time is {time}.", + 'intro_city': "This is the automated weather report for {city}.", + 'alert_advise': "Please be advised: ", + 'time_fmt': "%I %M %p" + }, + 'fr': { + 'wind_fmt': ", avec des vents du {dir} à {kph} kilomètres à l'heure", + 'current_intro': "Conditions actuelles pour {city}, {region}.", + 'temp_fmt': "La température est de {temp_c} degrés Celsius, {temp_f} degrés Fahrenheit.", + 'conditions_fmt': "Les conditions sont {desc}{wind}.", + 'forecast_intro': "Voici les prévisions. ", + 'high_fmt': " avec un maximum de {c} Celsius, {f} Fahrenheit", + 'low_fmt': " avec un minimum de {c} Celsius, {f} Fahrenheit", + 'period_fmt': "{period}: {summary}{temp}. ", + 'no_alerts': "Il n'y a aucune alerte météo en vigueur.", + 'alerts_intro': "Il y a {count} alertes météo en vigueur. ", + 'alert_item': "Un {title} est en vigueur jusqu'à {expires}. ", + 'greeting': "Bonjour. Il est {time}.", + 'intro_city': "Ceci est le bulletin météo automatisé pour {city}.", + 'alert_advise': "Veuillez noter : ", + 'time_fmt': "%H heures %M" + } + } + + def __init__(self, config: Dict): + self.lang = config.get('language', 'en') + self.s = self.STRINGS.get(self.lang, self.STRINGS['en']) def _c_to_f(self, temp_c: float) -> int: return int((temp_c * 9/5) + 32) + + def _t(self, key, **kwargs): + tpl = self.s.get(key, "") + return tpl.format(**kwargs) def announce_conditions(self, loc: LocationInfo, current: CurrentConditions) -> str: wind = "" if current.wind_speed and current.wind_speed > 5: - # Describe wind speed in km/h and mph mph = int(current.wind_speed * 0.621371) - wind = f", with winds from the {current.wind_direction} at {int(current.wind_speed)} kilometers per hour, or {mph} miles per hour" + wind = self._t('wind_fmt', dir=current.wind_direction, kph=int(current.wind_speed), mph=mph) + + intro = self._t('current_intro', city=loc.city, region=loc.region) + temp_str = self._t('temp_fmt', temp_c=int(current.temperature), temp_f=self._c_to_f(current.temperature)) + cond_str = self._t('conditions_fmt', desc=current.description, wind=wind) - return ( - f"Current conditions for {loc.city}, {loc.region}. " - f"The temperature is {int(current.temperature)} degrees celsius, {self._c_to_f(current.temperature)} degrees fahrenheit. " - f"Conditions are {current.description}{wind}." - ) + return f"{intro} {temp_str} {cond_str}" def announce_forecast(self, forecasts: List[WeatherForecast]) -> str: if not forecasts: return "" - text = "Here is the forecast. " - for f in forecasts[:3]: # Read first 3 periods + text = self._t('forecast_intro') + for f in forecasts[:3]: temp = "" if f.high_temp is not None: - temp = f" with a high of {int(f.high_temp)} celsius, {self._c_to_f(f.high_temp)} fahrenheit" + temp = self._t('high_fmt', c=int(f.high_temp), f=self._c_to_f(f.high_temp)) elif f.low_temp is not None: - temp = f" with a low of {int(f.low_temp)} celsius, {self._c_to_f(f.low_temp)} fahrenheit" + temp = self._t('low_fmt', c=int(f.low_temp), f=self._c_to_f(f.low_temp)) - text += f"{f.period_name}: {f.summary}{temp}. " + text += self._t('period_fmt', period=f.period_name, summary=f.summary, temp=temp) return text def announce_alerts(self, alerts: List[WeatherAlert]) -> str: if not alerts: - return "There are no active weather alerts." + return self._t('no_alerts') - text = f"There are {len(alerts)} active weather alerts. " + text = self._t('alerts_intro', count=len(alerts)) for a in alerts: - # "A Severe Thunderstorm Warning is in effect until 5 PM." - expires_str = a.expires.strftime("%I %M %p") - text += f"A {a.title} is in effect until {expires_str}. " + # Need locale specific time format? + # Alerts usually have specific expiration. + expires_str = a.expires.strftime(self.s.get('time_fmt', "%I %M %p")) + text += self._t('alert_item', title=a.title, expires=expires_str) return text def build_full_report(self, loc: LocationInfo, current: CurrentConditions, forecast: List[WeatherForecast], alerts: List[WeatherAlert], sun_info: str = "") -> str: parts = [] - - # Localize time try: tz = pytz.timezone(loc.timezone) now = datetime.now(tz) except Exception: now = datetime.now() - # Time string: "10 30 PM" - now_str = now.strftime("%I %M %p") - # Remove leading zero from hour if desired, but TTS usually handles "09" fine. - # For cleaner TTS: - if now_str.startswith("0"): + time_fmt = self.s.get('time_fmt', "%I %M %p") + now_str = now.strftime(time_fmt) + if self.lang == 'en' and now_str.startswith("0"): now_str = now_str[1:] - parts.append(f"Good day. The time is {now_str}.") - parts.append(f"This is the automated weather report for {loc.city}.") + parts.append(self._t('greeting', time=now_str)) + parts.append(self._t('intro_city', city=loc.city)) if alerts: - parts.append("Please be advised: " + self.announce_alerts(alerts)) + parts.append(self._t('alert_advise') + self.announce_alerts(alerts)) parts.append(self.announce_conditions(loc, current)) parts.append(self.announce_forecast(forecast)) diff --git a/asl3_wx_announce/provider/ec.py b/asl3_wx_announce/provider/ec.py index 30653c6..43f6c0d 100644 --- a/asl3_wx_announce/provider/ec.py +++ b/asl3_wx_announce/provider/ec.py @@ -8,10 +8,13 @@ class ECProvider(WeatherProvider): def __init__(self, **kwargs): self.points_cache = {} self.extra_zones = kwargs.get('alerts', {}).get('extra_zones', []) + + # Map config language code (fr/en) to ECWeather expected parameter (french/english) + self.language = 'french' if kwargs.get('language') == 'fr' else 'english' def _get_ec_data(self, lat, lon): # ECWeather auto-selects station based on lat/lon - ec = ECWeather(coordinates=(lat, lon)) + ec = ECWeather(coordinates=(lat, lon), language=self.language) ec.update() return ec @@ -62,7 +65,7 @@ class ECProvider(WeatherProvider): for zone_id in self.extra_zones: if "/" in zone_id: # Basic check if it looks like EC station ID try: - ec_objects.append(ECWeather(station_id=zone_id)) + ec_objects.append(ECWeather(station_id=zone_id, language=self.language)) except Exception: pass diff --git a/config.yaml.example b/config.yaml.example index 7dc603c..59def12 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -4,10 +4,14 @@ location: # longitude: -75.6972 # provider: CA # Optional: force US or CA +language: en # Options: en, fr + voice: # Example utilizing pico2wave (sudo apt install libttspico-utils) - tts_command: 'pico2wave -w /tmp/temp.wav "{text}" && sox /tmp/temp.wav -r 8000 -c 1 {file}' + # {lang} is replaced by 'en-US' or 'fr-FR' + tts_command: 'pico2wave -l {lang} -w /tmp/temp.wav "{text}" && sox /tmp/temp.wav -r 8000 -c 1 {file}' +audio: nodes: - "YOUR_NODE_NUMBER_HERE"