ea9c746852
The hand-written table understood "какая погода в X" for six values of X. Ask about Kazan or Tbilisi and the city was dropped silently and answered for the default location — a correct-sounding answer about the wrong place. The table is gone. internal/weather already calls Open-Meteo's geocoding endpoint on every lookup, so the place he named goes straight there and any place it knows is a place he can ask about. He speaks the prepositional case, so locationCandidates reverses the two endings that cover most of it: a final "е" is a nominative "а" or nothing, a final "и" is a soft sign. A wrong candidate finds no city; it never invents one. A place the geocoder does not have now reads as "не знаю такого города" rather than as a provider outage or, worse, as the default city's weather. ErrLocationUnknown is what carries that apart. "в" followed by a room or a day word is still the default location. Those questions are answered by the house sensors and the calendar, not by Open-Meteo, and they must not be read as a city.
71 lines
3.2 KiB
Go
71 lines
3.2 KiB
Go
// Package main — weatherq.go holds the weather-query keyword helpers: does
|
|
// this utterance ask about weather at all, and which place (if any) did he
|
|
// name. Both are plain keyword matching, not NLU — extend this file rather
|
|
// than voice.go for anything in that shape.
|
|
package main
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// isWeatherQuery returns true if the utterance is about weather.
|
|
func isWeatherQuery(u string) bool {
|
|
lower := strings.ToLower(u)
|
|
return strings.Contains(lower, "погод") ||
|
|
strings.Contains(lower, "градус") ||
|
|
strings.Contains(lower, "температур") ||
|
|
strings.Contains(lower, "дожд") ||
|
|
strings.Contains(lower, "холод") ||
|
|
strings.Contains(lower, "тепл") ||
|
|
strings.Contains(lower, "weather") ||
|
|
strings.Contains(lower, "temperature")
|
|
}
|
|
|
|
// weatherPlace — the place he named, after "в"/"во"/"in". One or two words,
|
|
// letters and dashes only, so "в Нижнем Новгороде" and "in New York" both
|
|
// come through whole and "в 5 утра" does not.
|
|
var weatherPlace = regexp.MustCompile(`(?i)(?:^|\s)(?:в|во|in)\s+([\p{L}-]+(?:\s+[\p{L}-]+)?)`)
|
|
|
|
// weatherNonPlaces — words that follow "в" in a weather question and are not
|
|
// cities. "какая погода в доме" is the smart-home sensor, not Open-Meteo, and
|
|
// "тепло в комнате" is the same question about the same room.
|
|
var weatherNonPlaces = map[string]bool{
|
|
"доме": true, "квартире": true, "комнате": true, "спальне": true,
|
|
"гостиной": true, "кухне": true, "гараже": true, "офисе": true,
|
|
"выходные": true, "субботу": true, "воскресенье": true, "понедельник": true,
|
|
"вторник": true, "среду": true, "четверг": true, "пятницу": true,
|
|
"обед": true, "обеде": true, "утро": true, "утром": true, "вечер": true,
|
|
"вечером": true, "ночь": true, "ночью": true, "целом": true, "принципе": true,
|
|
}
|
|
|
|
// extractWeatherLocation returns the place he named, or the configured default
|
|
// when he named none. It returns "" when he named none AND no default is
|
|
// configured — the caller must then say it does not know.
|
|
//
|
|
// It used to be a hand-written table of six cities in two spellings each
|
|
// (Vikunja #421). Anything outside it — Kazan, Tbilisi — was dropped silently
|
|
// and answered for the default location, which reads as a correct answer about
|
|
// the wrong place. There is a geocoder behind this now: internal/weather
|
|
// already calls Open-Meteo's geocoding endpoint for every lookup, so any place
|
|
// it knows is a place he can ask about, and the table bought nothing.
|
|
//
|
|
// A named place that the geocoder cannot resolve is the caller's problem to
|
|
// report, not this function's to hide.
|
|
//
|
|
// It used to return "Moscow" when he named nothing. That is a made-up answer
|
|
// presented as fact. voice.weather.default_location is the only source of an
|
|
// unstated location.
|
|
func extractWeatherLocation(u, defaultLoc string) string {
|
|
m := weatherPlace.FindStringSubmatch(u)
|
|
if m == nil {
|
|
return defaultLoc
|
|
}
|
|
place := strings.TrimSpace(m[1])
|
|
first := strings.ToLower(strings.Fields(place)[0])
|
|
if weatherNonPlaces[first] {
|
|
return defaultLoc
|
|
}
|
|
return place
|
|
}
|