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

def temperature(header: str, temp: avwx.structs.Number, unit: str = 'C') -> str:
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

def visibility(vis: avwx.structs.Number, unit: str = 'm') -> str:
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

def altimeter(alt: avwx.structs.Number, unit: str = 'inHg') -> str:
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

def wx_codes(codes: List[avwx.structs.Code]) -> str:
 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

def metar(data: avwx.structs.MetarData, units: avwx.structs.Units) -> str:
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

def taf_line(line: avwx.structs.TafLineData, units: avwx.structs.Units) -> str:
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

def taf(data: avwx.structs.TafData, units: avwx.structs.Units) -> str:
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