avwx.parsing.speech
Contains functions for converting translations into a speech string Currently only supports METAR
1""" 2Contains functions for converting translations into a speech string 3Currently only supports METAR 4""" 5 6# pylint: disable=redefined-builtin 7 8# stdlib 9import re 10from typing import List, Optional 11 12# module 13import avwx.parsing.translate.base as translate_base 14import avwx.parsing.translate.taf as translate_taf 15from avwx.parsing import core 16from avwx.static.core import SPOKEN_UNITS 17from avwx.structs import Code, MetarData, Number, TafData, TafLineData, Timestamp, Units 18 19 20def ordinal(n: int) -> Optional[str]: # pylint: disable=invalid-name 21 """Converts an int to it spoken ordinal representation""" 22 if n < 0: 23 return None 24 return str(n) + "tsnrhtdd"[(n / 10 % 10 != 1) * (n % 10 < 4) * n % 10 :: 4] 25 26 27def _format_plural_unit(value: str, unit: str) -> str: 28 spoken = SPOKEN_UNITS.get(unit, unit) 29 value = re.sub(r"(?<=\b1)" + unit, f" {spoken}", value) # 1 knot 30 value = re.sub(r"(?<=\d)+" + unit, f" {spoken}s", value) # 2 knots 31 return value 32 33 34def wind( 35 direction: Number, 36 speed: Number, 37 gust: Optional[Number], 38 vardir: Optional[List[Number]] = None, 39 unit: str = "kt", 40) -> str: 41 """Format wind details into a spoken word string""" 42 val = translate_base.wind( 43 direction, speed, gust, vardir, unit, cardinals=False, spoken=True 44 ) 45 if val and unit in SPOKEN_UNITS: 46 val = _format_plural_unit(val, unit) 47 return "Winds " + (val or "unknown") 48 49 50def temperature(header: str, temp: Number, unit: str = "C") -> str: 51 """Format temperature details into a spoken word string""" 52 if not (temp and temp.value): 53 return f"{header} unknown" 54 unit = SPOKEN_UNITS.get(unit, unit) 55 use_s = "" if temp.spoken in ("one", "minus one") else "s" 56 return " ".join((header, temp.spoken, f"degree{use_s}", unit)) 57 58 59def visibility(vis: Number, unit: str = "m") -> str: 60 """Format visibility details into a spoken word string""" 61 if not vis: 62 return "Visibility unknown" 63 if vis.value is None or "/" in vis.repr: 64 ret_vis = vis.spoken 65 else: 66 ret_vis = translate_base.visibility(vis, unit=unit) 67 if unit == "m": 68 unit = "km" 69 ret_vis = ret_vis[: ret_vis.find(" (")].lower().replace(unit, "").strip() 70 ret_vis = core.spoken_number(core.remove_leading_zeros(ret_vis)) 71 ret = f"Visibility {ret_vis}" 72 if unit in SPOKEN_UNITS: 73 if "/" in vis.repr and "half" not in ret: 74 ret += " of a" 75 ret += f" {SPOKEN_UNITS[unit]}" 76 if ("one half" not in ret or " and " in ret) and "of a" not in ret: 77 ret += "s" 78 else: 79 ret += unit 80 return ret 81 82 83def altimeter(alt: Number, unit: str = "inHg") -> str: 84 """Format altimeter details into a spoken word string""" 85 ret = "Altimeter " 86 if not alt: 87 ret += "unknown" 88 elif unit == "inHg": 89 ret += core.spoken_number(str(alt.value).ljust(5, "0"), True) 90 elif unit == "hPa": 91 ret += core.spoken_number(str(alt.value).zfill(4), True) 92 return ret 93 94 95def wx_codes(codes: List[Code]) -> str: 96 """ 97 Format wx codes into a spoken word string 98 """ 99 ret = [] 100 for code in codes: 101 item = code.value 102 if item.startswith("Vicinity"): 103 item = item.lstrip("Vicinity ") + " in the Vicinity" 104 ret.append(item) 105 return ". ".join(ret) 106 107 108def type_and_times( 109 type: str, 110 start: Optional[Timestamp], 111 end: Optional[Timestamp], 112 probability: Optional[Number] = None, 113) -> str: 114 """Format line type and times into the beginning of a spoken line string""" 115 if not type: 116 return "" 117 start_time = start.dt.hour if start and start.dt else "an unknown start time" 118 end_time = end.dt.hour if end and end.dt else "an unknown end time" 119 if type == "BECMG": 120 return f"At {start_time or 'midnight'} zulu becoming" 121 ret = f"From {start_time or 'midnight'} to {end_time or 'midnight'} zulu," 122 if probability and probability.value: 123 ret += f" there's a {probability.value}% chance for" 124 if type == "INTER": 125 ret += " intermittent" 126 elif type == "TEMPO": 127 ret += " temporary" 128 return ret 129 130 131def wind_shear(shear: str, unit_alt: str = "ft", unit_wind: str = "kt") -> str: 132 """Format wind shear string into a spoken word string""" 133 value = translate_taf.wind_shear(shear, unit_alt, unit_wind, spoken=True) 134 if not value: 135 return "Wind shear unknown" 136 for unit in (unit_alt, unit_wind): 137 if unit in SPOKEN_UNITS: 138 value = _format_plural_unit(value, unit) 139 return value 140 141 142def metar(data: MetarData, units: Units) -> str: 143 """Convert MetarData into a string for text-to-speech""" 144 speech = [] 145 if data.wind_direction and data.wind_speed: 146 speech.append( 147 wind( 148 data.wind_direction, 149 data.wind_speed, 150 data.wind_gust, 151 data.wind_variable_direction, 152 units.wind_speed, 153 ) 154 ) 155 if data.visibility: 156 speech.append(visibility(data.visibility, units.visibility)) 157 if data.temperature: 158 speech.append(temperature("Temperature", data.temperature, units.temperature)) 159 if data.dewpoint: 160 speech.append(temperature("Dew point", data.dewpoint, units.temperature)) 161 if data.altimeter: 162 speech.append(altimeter(data.altimeter, units.altimeter)) 163 if data.wx_codes: 164 speech.append(wx_codes(data.wx_codes)) 165 speech.append( 166 translate_base.clouds(data.clouds, units.altitude).replace( 167 " - Reported AGL", "" 168 ) 169 ) 170 return (". ".join([l for l in speech if l])).replace(",", ".") 171 172 173def taf_line(line: TafLineData, units: Units) -> str: 174 """Convert TafLineData into a string for text-to-speech""" 175 speech = [] 176 start = type_and_times(line.type, line.start_time, line.end_time, line.probability) 177 if line.wind_direction and line.wind_speed: 178 speech.append( 179 wind( 180 line.wind_direction, 181 line.wind_speed, 182 line.wind_gust, 183 line.wind_variable_direction, 184 unit=units.wind_speed, 185 ) 186 ) 187 if line.wind_shear: 188 speech.append(wind_shear(line.wind_shear, units.altitude, units.wind_speed)) 189 if line.visibility: 190 speech.append(visibility(line.visibility, units.visibility)) 191 if line.altimeter: 192 speech.append(altimeter(line.altimeter, units.altimeter)) 193 if line.wx_codes: 194 speech.append(wx_codes(line.wx_codes)) 195 speech.append( 196 translate_base.clouds(line.clouds, units.altitude).replace( 197 " - Reported AGL", "" 198 ) 199 ) 200 if line.turbulence: 201 speech.append(translate_taf.turb_ice(line.turbulence, units.altitude)) 202 if line.icing: 203 speech.append(translate_taf.turb_ice(line.icing, units.altitude)) 204 return f"{start} " + (". ".join([l for l in speech if l])).replace(",", ".") 205 206 207def taf(data: TafData, units: Units) -> str: 208 """Convert TafData into a string for text-to-speech""" 209 try: 210 month = data.start_time.dt.strftime(r"%B") # type: ignore 211 day = ordinal(data.start_time.dt.day) or "Unknown" # type: ignore 212 ret = f"Starting on {month} {day} - " 213 except AttributeError: 214 ret = "" 215 return ret + ". ".join([taf_line(line, units) for line in data.forecast])
def
ordinal(n: int) -> Optional[str]:
21def ordinal(n: int) -> Optional[str]: # pylint: disable=invalid-name 22 """Converts an int to it spoken ordinal representation""" 23 if n < 0: 24 return None 25 return str(n) + "tsnrhtdd"[(n / 10 % 10 != 1) * (n % 10 < 4) * n % 10 :: 4]
Converts an int to it spoken ordinal representation
def
wind( direction: avwx.structs.Number, speed: avwx.structs.Number, gust: Optional[avwx.structs.Number], vardir: Optional[List[avwx.structs.Number]] = None, unit: str = 'kt') -> str:
35def wind( 36 direction: Number, 37 speed: Number, 38 gust: Optional[Number], 39 vardir: Optional[List[Number]] = None, 40 unit: str = "kt", 41) -> str: 42 """Format wind details into a spoken word string""" 43 val = translate_base.wind( 44 direction, speed, gust, vardir, unit, cardinals=False, spoken=True 45 ) 46 if val and unit in SPOKEN_UNITS: 47 val = _format_plural_unit(val, unit) 48 return "Winds " + (val or "unknown")
Format wind details into a spoken word string
51def temperature(header: str, temp: Number, unit: str = "C") -> str: 52 """Format temperature details into a spoken word string""" 53 if not (temp and temp.value): 54 return f"{header} unknown" 55 unit = SPOKEN_UNITS.get(unit, unit) 56 use_s = "" if temp.spoken in ("one", "minus one") else "s" 57 return " ".join((header, temp.spoken, f"degree{use_s}", unit))
Format temperature details into a spoken word string
60def visibility(vis: Number, unit: str = "m") -> str: 61 """Format visibility details into a spoken word string""" 62 if not vis: 63 return "Visibility unknown" 64 if vis.value is None or "/" in vis.repr: 65 ret_vis = vis.spoken 66 else: 67 ret_vis = translate_base.visibility(vis, unit=unit) 68 if unit == "m": 69 unit = "km" 70 ret_vis = ret_vis[: ret_vis.find(" (")].lower().replace(unit, "").strip() 71 ret_vis = core.spoken_number(core.remove_leading_zeros(ret_vis)) 72 ret = f"Visibility {ret_vis}" 73 if unit in SPOKEN_UNITS: 74 if "/" in vis.repr and "half" not in ret: 75 ret += " of a" 76 ret += f" {SPOKEN_UNITS[unit]}" 77 if ("one half" not in ret or " and " in ret) and "of a" not in ret: 78 ret += "s" 79 else: 80 ret += unit 81 return ret
Format visibility details into a spoken word string
84def altimeter(alt: Number, unit: str = "inHg") -> str: 85 """Format altimeter details into a spoken word string""" 86 ret = "Altimeter " 87 if not alt: 88 ret += "unknown" 89 elif unit == "inHg": 90 ret += core.spoken_number(str(alt.value).ljust(5, "0"), True) 91 elif unit == "hPa": 92 ret += core.spoken_number(str(alt.value).zfill(4), True) 93 return ret
Format altimeter details into a spoken word string
96def wx_codes(codes: List[Code]) -> str: 97 """ 98 Format wx codes into a spoken word string 99 """ 100 ret = [] 101 for code in codes: 102 item = code.value 103 if item.startswith("Vicinity"): 104 item = item.lstrip("Vicinity ") + " in the Vicinity" 105 ret.append(item) 106 return ". ".join(ret)
Format wx codes into a spoken word string
def
type_and_times( type: str, start: Optional[avwx.structs.Timestamp], end: Optional[avwx.structs.Timestamp], probability: Optional[avwx.structs.Number] = None) -> str:
109def type_and_times( 110 type: str, 111 start: Optional[Timestamp], 112 end: Optional[Timestamp], 113 probability: Optional[Number] = None, 114) -> str: 115 """Format line type and times into the beginning of a spoken line string""" 116 if not type: 117 return "" 118 start_time = start.dt.hour if start and start.dt else "an unknown start time" 119 end_time = end.dt.hour if end and end.dt else "an unknown end time" 120 if type == "BECMG": 121 return f"At {start_time or 'midnight'} zulu becoming" 122 ret = f"From {start_time or 'midnight'} to {end_time or 'midnight'} zulu," 123 if probability and probability.value: 124 ret += f" there's a {probability.value}% chance for" 125 if type == "INTER": 126 ret += " intermittent" 127 elif type == "TEMPO": 128 ret += " temporary" 129 return ret
Format line type and times into the beginning of a spoken line string
def
wind_shear(shear: str, unit_alt: str = 'ft', unit_wind: str = 'kt') -> str:
132def wind_shear(shear: str, unit_alt: str = "ft", unit_wind: str = "kt") -> str: 133 """Format wind shear string into a spoken word string""" 134 value = translate_taf.wind_shear(shear, unit_alt, unit_wind, spoken=True) 135 if not value: 136 return "Wind shear unknown" 137 for unit in (unit_alt, unit_wind): 138 if unit in SPOKEN_UNITS: 139 value = _format_plural_unit(value, unit) 140 return value
Format wind shear string into a spoken word string
143def metar(data: MetarData, units: Units) -> str: 144 """Convert MetarData into a string for text-to-speech""" 145 speech = [] 146 if data.wind_direction and data.wind_speed: 147 speech.append( 148 wind( 149 data.wind_direction, 150 data.wind_speed, 151 data.wind_gust, 152 data.wind_variable_direction, 153 units.wind_speed, 154 ) 155 ) 156 if data.visibility: 157 speech.append(visibility(data.visibility, units.visibility)) 158 if data.temperature: 159 speech.append(temperature("Temperature", data.temperature, units.temperature)) 160 if data.dewpoint: 161 speech.append(temperature("Dew point", data.dewpoint, units.temperature)) 162 if data.altimeter: 163 speech.append(altimeter(data.altimeter, units.altimeter)) 164 if data.wx_codes: 165 speech.append(wx_codes(data.wx_codes)) 166 speech.append( 167 translate_base.clouds(data.clouds, units.altitude).replace( 168 " - Reported AGL", "" 169 ) 170 ) 171 return (". ".join([l for l in speech if l])).replace(",", ".")
Convert MetarData into a string for text-to-speech
174def taf_line(line: TafLineData, units: Units) -> str: 175 """Convert TafLineData into a string for text-to-speech""" 176 speech = [] 177 start = type_and_times(line.type, line.start_time, line.end_time, line.probability) 178 if line.wind_direction and line.wind_speed: 179 speech.append( 180 wind( 181 line.wind_direction, 182 line.wind_speed, 183 line.wind_gust, 184 line.wind_variable_direction, 185 unit=units.wind_speed, 186 ) 187 ) 188 if line.wind_shear: 189 speech.append(wind_shear(line.wind_shear, units.altitude, units.wind_speed)) 190 if line.visibility: 191 speech.append(visibility(line.visibility, units.visibility)) 192 if line.altimeter: 193 speech.append(altimeter(line.altimeter, units.altimeter)) 194 if line.wx_codes: 195 speech.append(wx_codes(line.wx_codes)) 196 speech.append( 197 translate_base.clouds(line.clouds, units.altitude).replace( 198 " - Reported AGL", "" 199 ) 200 ) 201 if line.turbulence: 202 speech.append(translate_taf.turb_ice(line.turbulence, units.altitude)) 203 if line.icing: 204 speech.append(translate_taf.turb_ice(line.icing, units.altitude)) 205 return f"{start} " + (". ".join([l for l in speech if l])).replace(",", ".")
Convert TafLineData into a string for text-to-speech
208def taf(data: TafData, units: Units) -> str: 209 """Convert TafData into a string for text-to-speech""" 210 try: 211 month = data.start_time.dt.strftime(r"%B") # type: ignore 212 day = ordinal(data.start_time.dt.day) or "Unknown" # type: ignore 213 ret = f"Starting on {month} {day} - " 214 except AttributeError: 215 ret = "" 216 return ret + ". ".join([taf_line(line, units) for line in data.forecast])
Convert TafData into a string for text-to-speech