9e1958e7b0
router.WeekdayIndex reads the lexicon and asks the dictionary about the case. Four private lists go away: the habit declension map, the weekday block of the day-plan refusal, the weekday and part-of-day entries of the weather guard, and the stem list in ruwords.go. The stem list was the real defect. mentionsUnknownDay matched sred, pyatnits and subbot with strings.Contains, so sredi, sredstvo and sredniy all read as Wednesday and a question carrying one was answered with onlyNearDaysReply instead of a date. It matches whole tokens now. The weather guard was a third copy of three closed sets that already exist. It kept the rooms of the house, which are its own, and asks the lexicon for the weekdays, the parts of the day and the words that follow v without naming a place. Questions phrased v srede, v utra and v obshchem reached the geocoder as cities before. Full suite green under -race.
101 lines
4.0 KiB
Go
101 lines
4.0 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"
|
|
|
|
"github.com/kami/maven/internal/lexicon"
|
|
"github.com/kami/maven/internal/morph"
|
|
"github.com/kami/maven/internal/router"
|
|
)
|
|
|
|
// 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}-]+)?)`)
|
|
|
|
// weatherRooms — the rooms of the house, which are the only words in this
|
|
// guard that belong to it. "какая погода в доме" is the smart-home sensor, not
|
|
// Open-Meteo, and "тепло в комнате" is the same question about the same room.
|
|
//
|
|
// The rest of the guard used to be a third copy of three closed sets that
|
|
// already exist in the lexicon: the weekdays, the parts of the day, and the
|
|
// words that follow "в" without naming a place (V-581). Each copy was short in
|
|
// its own direction — "среду" but not "среде", "утром" but not "утра", "целом"
|
|
// but not "общем" — so the same question phrased one word differently reached
|
|
// the geocoder as a city.
|
|
var weatherRooms = map[string]bool{
|
|
"доме": true, "квартире": true, "комнате": true, "спальне": true,
|
|
"гостиной": true, "кухне": true, "гараже": true, "офисе": true,
|
|
"обед": true, "обеде": true, "выходные": true, "выходных": true,
|
|
}
|
|
|
|
// isWeatherNonPlace reports whether the word after "в" names something other
|
|
// than a place he could ask the weather for.
|
|
func isWeatherNonPlace(word string) bool {
|
|
if weatherRooms[word] {
|
|
return true
|
|
}
|
|
if _, ok := router.WeekdayIndex(word); ok {
|
|
return true
|
|
}
|
|
for _, w := range lexicon.PartsOfDay() {
|
|
if word == w || morph.SameWord(word, w) {
|
|
return true
|
|
}
|
|
}
|
|
for _, w := range lexicon.NotPlaceAfterV() {
|
|
if word == w {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// 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 isWeatherNonPlace(first) {
|
|
return defaultLoc
|
|
}
|
|
return place
|
|
}
|