// Package ttsnorm rewrites machine-formatted dates/times/numbers into RU text // a TTS voice speaks naturally — so "10.07.2026" is not read as "number dot // number dot number". Pure, deterministic; runs on reply/nudge text before synth. package ttsnorm import ( "regexp" "strconv" "strings" "github.com/kami/maven/internal/lexicon" ) // The month names are a closed class and live in internal/lexicon, 1-indexed, // which is also where the voice reply path reads them. There used to be a second // copy of the twelve names in cmd/mavend/ruwords.go (Vikunja #525). var ( reDateY = regexp.MustCompile(`\b(\d{1,2})\.(\d{1,2})\.(\d{4})\b`) reDate = regexp.MustCompile(`\b(\d{1,2})\.(\d{1,2})\b`) reTime = regexp.MustCompile(`\b(\d{1,2}):(\d{2})\b`) reDots = regexp.MustCompile(`\b\d+(?:\.\d+){2,}\b`) ) // Speakable rewrites d.m.y, d.m, h:mm, and residual dotted-number runs. // Order matters: dates with year first, then times, then multi-dot numbers // (3+ parts — never valid dates), then 2-part dates. func Speakable(s string) string { s = reDateY.ReplaceAllStringFunc(s, func(m string) string { p := reDateY.FindStringSubmatch(m) return spokenDate(p[1], p[2], p[3]) }) s = reTime.ReplaceAllStringFunc(s, func(m string) string { p := reTime.FindStringSubmatch(m) return p[1] + " часов " + p[2] + " минут" }) s = reDots.ReplaceAllStringFunc(s, func(m string) string { return strings.Join(strings.Split(m, "."), " точка ") }) s = reDate.ReplaceAllStringFunc(s, func(m string) string { p := reDate.FindStringSubmatch(m) return spokenDate(p[1], p[2], "") }) // Last, so a hostname is spelled out after the numbers around it are // already words and no rewrite above can see Cyrillic it did not expect. return Pronounce(s) } func spokenDate(dd, mm, yyyy string) string { mi, _ := strconv.Atoi(mm) if mi < 1 || mi > 12 { return dd + " " + mm + gap(yyyy) } day := strconv.Itoa(mustInt(dd)) out := day + " " + lexicon.MonthGenitive(mi) if yyyy != "" { out += " " + yyyy } return out } func mustInt(s string) int { n, _ := strconv.Atoi(s); return n } func gap(y string) string { if y == "" { return "" } return " " + y }