voice/seeds: track seed files, add russian time parser, guard replySystem

five fixes spotted during routing investigation:

- gitignore: replace blanket models/ ignore with per-dir exceptions
  (/models/embedder/, /models/stt/, /models/tts/) so the seed text
  files under models/seeds/ are tracked in version control
- query.txt: fix merged line — 'сколько стоит свет в этом месяце' and
  'найди заметку про сервер' were fused with no separator
- reminder.txt: add 11 pure-verb reminder seeds without time expressions
  to shift centroid toward the reminding intent rather than time-lexicon
- StubDateTimeParser: add Russian 'через <N> <unit>'/'через час'/'через
  полчаса', 'сегодня'/'завтра'/'послезавтра' with optional clock, and
  'в <clock>' scan. Add Russian word numbers (один-десять) and unit
  inflections (час/часа/часов, минута/минуты/минут, день/дня/дней,
  неделя/недели/недель). Also adds missing English day/week units.
- replySystem: guard time branch against duration queries ('сколько
  времени прошло') reaching it via the classifier path after the
  stage-0 grammar's build filter rejects them. Mirrors stage0.go
  duration keywords.
This commit is contained in:
kami
2026-07-06 18:38:48 +04:00
parent ce3d8e65f2
commit bf4009ca4f
9 changed files with 361 additions and 3 deletions
+5 -2
View File
@@ -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
+30
View File
@@ -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
+83
View File
@@ -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) ---
// "в <clock>" 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
}
}
// "через <N> <unit>" / "через <unit>" (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: "сегодня", "завтра", "послезавтра" [в] <clock>
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
}
+69
View File
@@ -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
перезагрузи сервер
останови контейнер
перезапусти сервис
останови сервис
запусти сервис
перезагрузи систему
+40
View File
@@ -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
+34
View File
@@ -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
запиши что нужно купить в магазине
заметка: переклеить обои в спальне
запиши время работы химчистки
+2 -1
View File
@@ -35,7 +35,8 @@ how many hours did I sleep this week
покажи логи за сегодня
кто последний раз делал бэкап
какой ip адрес у сервера
сколько стоит свет в этом месяценайди заметку про сервер
сколько стоит свет в этом месяце
найди заметку про сервер
найди мою заметку о бэкапах
поищи заметку про роутер
найди заметку где я записал пароль
+47
View File
@@ -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
+51
View File
@@ -0,0 +1,51 @@
который час
сколько времени
какой сегодня день
какое сегодня число
который час в Москве
сколько сейчас времени
который час у нас
сколько времени осталось до вечера
какой сегодня день недели
какая температура воздуха
сколько человек дома
кто сейчас дома
есть ли кто дома
кто дома сейчас
все ли дома
сколько памяти занято
какая загрузка процессора
сколько свободного места на диске
какой ip адрес у сервера
как дела у сервера
всё ли работает
сколько сервер работает без перезагрузки
когда сервер запускался
какая версия софта
сколько аптайм
какой статус сервисов
все ли сервисы работают
что с интернетом
интернет работает
когда последний раз перезагружался
сколько трафика сегодня
какая скорость интернета
загрузка сети
сколько процессов запущено
как загрузка системы
сколько оперативной памяти свободно
какая температура процессора
тихий режим
тихо
не шуми
тихий режим включи
включи тихий режим
не беспокоить
выключи тихий режим
шумный режим
громкий режим
отключи тихий режим
quiet mode on
quiet mode off
quiet on
quiet off