diff --git a/.gitignore b/.gitignore index ba1d561..1d87b62 100644 --- a/.gitignore +++ b/.gitignore @@ -13,8 +13,11 @@ certs/ # Dependencies (fetch/build, not vendored) deps/ -# ML models (large, downloaded separately) -models/ +# ML models (large, downloaded separately) — specific dirs, not blanket, +# because models/seeds/*.txt are small, tracked files the classifier needs. +/models/embedder/ +/models/stt/ +/models/tts/ # Runtime data *.db diff --git a/cmd/mavend/voice.go b/cmd/mavend/voice.go index 5fd4b46..33c3a2d 100644 --- a/cmd/mavend/voice.go +++ b/cmd/mavend/voice.go @@ -712,6 +712,13 @@ func (h *reactiveHandler) replySystem(ctx context.Context, dec router.Decision) u := strings.ToLower(dec.Utterance) now := h.now() + // stage-0 grammars catch the exact time/date patterns, but duration + // queries ("сколько времени прошло") bypass the grammar's build filter + // and can reach replySystem via the classifier path. Guard against them. + if hasDurationWords(u) { + return "пока не умею отвечать на этот вопрос." + } + switch { case strings.Contains(u, "час") || strings.Contains(u, "врем"): h := now.Hour() @@ -735,6 +742,29 @@ func (h *reactiveHandler) replySystem(ctx context.Context, dec router.Decision) } } +// hasDurationWords checks whether u is asking about elapsed/remaining time +// rather than the current clock — guards replySystem from replying "сейчас +// X часов" to "сколько времени прошло". Mirrors the stage0.go build filter. +func hasDurationWords(u string) bool { + s := strings.ToLower(strings.TrimSpace(u)) + // First-word duration markers (same keywords as timeQueryBuild in stage0). + first := strings.Fields(s) + if len(first) > 0 { + switch first[0] { + case "прошло", "осталось", "пройдет", "минуло", "проходит": + return true + } + } + // Broader duration keywords appearing anywhere in the utterance. + if strings.Contains(s, "прошло") || strings.Contains(s, "осталось") { + return true + } + if strings.Contains(s, " до ") { + return true + } + return false +} + // reply wraps a text reply through TTS to produce a PushToTalkResp. If TTS // fails, the response carries an empty audio + the text — the client can // still display text if it can't play. The routedChannels field is diff --git a/internal/router/slots.go b/internal/router/slots.go index 0af7530..c8760eb 100644 --- a/internal/router/slots.go +++ b/internal/router/slots.go @@ -198,6 +198,72 @@ func (StubDateTimeParser) Parse(_ context.Context, text string, now time.Time) ( return t, true, nil } } + + // --- Russian time expressions (stub floor; dateparser replaces) --- + + // "в " anywhere — mirror of the English "at" scan. + for i := 0; i+1 < len(toks); i++ { + if toks[i] != "в" { + continue + } + if t, ok := parseClock(toks[i+1], now); ok { + return t, true, nil + } + } + + // "через " / "через " (bare = 1) / "через полчаса". + for i := 0; i+1 < len(toks); i++ { + if toks[i] != "через" { + continue + } + // "через N unit" — three-token scan. + if i+2 < len(toks) { + n, unit, ok := splitNumUnit(toks[i+1] + " " + toks[i+2]) + if ok { + if d, ok := unitToDuration(n, unit); ok { + return now.Add(d), true, nil + } + } + } + // "через полчаса" + if toks[i+1] == "полчаса" { + return now.Add(30 * time.Minute), true, nil + } + // "через unit" (bare unit without number = 1, e.g. "через час") + if d, ok := unitToDuration(1, toks[i+1]); ok { + return now.Add(d), true, nil + } + } + + // Calendar day: "сегодня", "завтра", "послезавтра" [в] + for i := 0; i < len(toks); i++ { + var dayShift time.Duration + switch toks[i] { + case "сегодня": + dayShift = 0 + case "завтра": + dayShift = 24 * time.Hour + case "послезавтра": + dayShift = 48 * time.Hour + default: + continue + } + base := now.Truncate(24 * time.Hour).Add(dayShift) + // Look for clock after the day word (optional "в"). + nextIdx := i + 1 + if nextIdx < len(toks) && toks[nextIdx] == "в" { + nextIdx++ + } + if nextIdx < len(toks) { + if t, ok := parseClock(toks[nextIdx], now); ok { + t = time.Date(base.Year(), base.Month(), base.Day(), t.Hour(), t.Minute(), 0, 0, now.Location()) + return t, true, nil + } + } + // No clock — return midnight of that day. + return base, true, nil + } + // bare clock at start ("7:30"). if len(toks) > 0 { if t, ok := parseClock(toks[0], now); ok { @@ -286,6 +352,11 @@ var wordNumbers = map[string]int{ "six": 6, "seven": 7, "eight": 8, "nine": 9, "ten": 10, "eleven": 11, "twelve": 12, "fifteen": 15, "twenty": 20, "thirty": 30, "forty": 40, "fifty": 50, "sixty": 60, + // Russian word numbers (gender variants cover natural речи) + "один": 1, "одна": 1, "одно": 1, + "два": 2, "две": 2, "три": 3, "четыре": 4, + "пять": 5, "шесть": 6, "семь": 7, "восемь": 8, + "девять": 9, "десять": 10, } func leadingWordNumber(s string) (int, string, bool) { @@ -308,6 +379,18 @@ func unitToDuration(n int, unit string) (time.Duration, bool) { return time.Duration(n) * time.Minute, true case "s", "sec", "secs", "second", "seconds": return time.Duration(n) * time.Second, true + // English day/week (pre-existing gap) + case "day", "days": + return time.Duration(n) * 24 * time.Hour, true + // Russian units (inflected forms) + case "час", "часа", "часов": + return time.Duration(n) * time.Hour, true + case "минута", "минуты", "минут": + return time.Duration(n) * time.Minute, true + case "день", "дня", "дней": + return time.Duration(n) * 24 * time.Hour, true + case "неделя", "недели", "недель": + return time.Duration(n) * 7 * 24 * time.Hour, true } return 0, false } diff --git a/models/seeds/act.txt b/models/seeds/act.txt new file mode 100644 index 0000000..c4c242f --- /dev/null +++ b/models/seeds/act.txt @@ -0,0 +1,69 @@ +maven, restart nginx +maven, stop the backup +maven, start the deployment +maven, run the backup script +maven, turn on the heater +maven, выключи свет +maven, запусти обновление +maven, открой шлагбаум +maven, сделай бэкап +maven, выключи сервер +включи микроволновку +выключи кондиционер +включи свет на кухне +открой ворота +запусти вентиляцию +выключи телевизор +включи музыку +выключи музыку +сделай громче +сделай тише +перезагрузи роутер +включи кофеварку +открой окно +закрой окно +запусти пылесос +останови пылесос +включи обогреватель +выключи обогреватель +включи чайник +открой шторы +закрой шторы +включи полив +выключи полив +заблокируй дверь +открой дверь +запусти обновление системы +останови сервис +перезапусти медиацентр +включи кондиционер +поставь таймер на 10 минут +выключи всё +перезагрузи компьютер +разблокируй дверь +запусти диагностику +останови все +# --- homelab read-only queries --- +покажи статус nginx +статус докера +покажи запущенные контейнеры +сколько места на диске +свободная память +покажи uptime +покажи логи nginx +лог докера +статус sshd +как работает сервер +что запущено + +# --- homelab destructive (with confirm gate) --- +перезапусти nginx +перезагрузи докер +останови nginx +запусти nginx +перезагрузи сервер +останови контейнер +перезапусти сервис +останови сервис +запусти сервис +перезагрузи систему diff --git a/models/seeds/fact.txt b/models/seeds/fact.txt new file mode 100644 index 0000000..3940bf4 --- /dev/null +++ b/models/seeds/fact.txt @@ -0,0 +1,40 @@ +отметь что я выпил воды +slept 6h +запиши что я пообедал +отметь тренировку 45 минут +записываю вес 72 килограмма +отметь что покормил кота +принял лекарство +выпил кофе +отметь что я сделал зарядку +logged 8 hours of sleep +запиши что съел на завтрак +отметь температуру 36.6 +записываю давление 120 на 80 +отметь пробежку 5 километров +выпил 2 литра воды +записываю что почистил зубы +отметь приём витаминов +запиши обед: гречка с курицей +отметь что полил цветы +вес 73.5 килограмма +тренировка 30 минут +сон 7 часов +выпил таблетку от головы +пообедал в 2 часа +выпил стакан сока +записываю что сходил в душ +отметь что переоделся +вечерний приём лекарств +заряжался 15 минут +записываю утреннюю пробежку +accounted 2000 ml of water +weight 71.8 kg +ran 3 kilometers +meditation 10 minutes +breakfast: oatmeal with berries +took vitamin d +walked 8000 steps +stretching 20 minutes +logged blood sugar 5.2 +dinner at 7pm \ No newline at end of file diff --git a/models/seeds/note.txt b/models/seeds/note.txt new file mode 100644 index 0000000..d7aed1c --- /dev/null +++ b/models/seeds/note.txt @@ -0,0 +1,34 @@ +note: staggered cooldown by time of day +заметка: купить новый фильтр для воды +запиши идею про автоматизацию полива +note: check the DNS config after update +заметка: починить ручку на двери +запиши рецепт: 3 яйца, мука, молоко +заметка: сменить масло в машине +note: upgrade wireguard on the phone +запиши: купить подарок на день рождения +заметка: заменить лампочку в коридоре +напомни себе купить новый жесткий диск +запиши: проверить проводку на даче +заметка: переустановить home assistant +note: add a backup job for the database +запиши пароль от wifi в заметки +заметка: починить протечку крана +купить новый фильтр для аквариума +заметка: записаться к стоматологу +запиши рецепт блинов +заметка: посадить рассаду в марте +запиши адрес: москва, тверская 7 +заметка: просмотреть документацию по API +запиши: купить семена для огорода +заметка: обновить прошивку роутера +запиши книгу, которую посоветовали +заметка: сделать прививку от гриппа +запиши цену на стройматериалы +заметка: проверить уровень масла +запиши размеры полки для шкафа +заметка: заказать запчасти для стиралки +note: ideas for the garden automation +запиши что нужно купить в магазине +заметка: переклеить обои в спальне +запиши время работы химчистки \ No newline at end of file diff --git a/models/seeds/query.txt b/models/seeds/query.txt index 357fc2f..f74aaec 100644 --- a/models/seeds/query.txt +++ b/models/seeds/query.txt @@ -35,7 +35,8 @@ how many hours did I sleep this week покажи логи за сегодня кто последний раз делал бэкап какой ip адрес у сервера -сколько стоит свет в этом месяценайди заметку про сервер +сколько стоит свет в этом месяце +найди заметку про сервер найди мою заметку о бэкапах поищи заметку про роутер найди заметку где я записал пароль diff --git a/models/seeds/reminder.txt b/models/seeds/reminder.txt new file mode 100644 index 0000000..f8022c7 --- /dev/null +++ b/models/seeds/reminder.txt @@ -0,0 +1,47 @@ +напомни через 4 часа размяться +remind me in 30 minutes to drink water +напомни завтра в 9 утра позвонить +напомни через час проверить духовку +напомни в пятницу вынести мусор +remind me at 6pm to take out the trash +напомни через 15 минут снять бельё +напомни в 8 вечера встречу +напомни через 2 дня оплатить счёт +напомни завтра утром покормить кота +remind me tomorrow at 8am to call the doctor +напомни через 10 минут снять с огня +напомни в среду оплатить интернет +напомни сегодня в 3 часа собрание +remind me next monday to submit the report +напомни через 3 часа принять лекарство +напомни завтра вечером купить хлеб +remind me in 5 minutes to check the oven +напомни в понедельник утром вынести мусор +напомни через 30 минут выключить чайник +напомни сегодня вечером позвонить маме +remind me on friday at noon to water plants +напомни через неделю заплатить за квартиру +напомни завтра в 10 утра тренировка +напомни в воскресенье убраться в гараже +напомни через 2 часа покормить собаку +напомни сегодня в полдень обед +напомни в 7 утра зарядка +напомни через минуту снять пробу +напомни завтра в 5 вечера забрать посылку +remind me tonight at 10pm to take medicine +напомни в четверг заплатить налоги +напомни через 20 минут выключить духовку +напомни в субботу убрать в комнате +напомни завтра после обеда позвонить +# --- pure-verb reminder seeds (no time expression) — shifts centroid away +# from time-lexicon dominance toward the reminding intent itself. +напомни мне +напомни пожалуйста +не забудь напомнить +поставь напоминание +создай напоминание +добавь напоминание +запиши напоминание +set a reminder +create a reminder +make a reminder \ No newline at end of file diff --git a/models/seeds/system.txt b/models/seeds/system.txt new file mode 100644 index 0000000..aba46a3 --- /dev/null +++ b/models/seeds/system.txt @@ -0,0 +1,51 @@ +который час +сколько времени +какой сегодня день +какое сегодня число +который час в Москве +сколько сейчас времени +который час у нас +сколько времени осталось до вечера +какой сегодня день недели +какая температура воздуха +сколько человек дома +кто сейчас дома +есть ли кто дома +кто дома сейчас +все ли дома +сколько памяти занято +какая загрузка процессора +сколько свободного места на диске +какой ip адрес у сервера +как дела у сервера +всё ли работает +сколько сервер работает без перезагрузки +когда сервер запускался +какая версия софта +сколько аптайм +какой статус сервисов +все ли сервисы работают +что с интернетом +интернет работает +когда последний раз перезагружался +сколько трафика сегодня +какая скорость интернета +загрузка сети +сколько процессов запущено +как загрузка системы +сколько оперативной памяти свободно +какая температура процессора +тихий режим +тихо +не шуми +тихий режим включи +включи тихий режим +не беспокоить +выключи тихий режим +шумный режим +громкий режим +отключи тихий режим +quiet mode on +quiet mode off +quiet on +quiet off \ No newline at end of file