// ttsnorm/pronounce.go — how piper says the latin words that turn up inside a // Russian sentence: service names, hostnames, acronyms (Vikunja #458). // // The RU voice reads latin letters one at a time or guesses, so "netdata" comes // out as noise and "homesrv" comes out as nothing. The fix is spelling the // sound in Cyrillic, and the mapping is data — nothing in the code knows any of // these names, and adding one is an edit to pronounce_ru_v1.json. // // This is a rewrite over a latin token, not over Russian morphology: the // pattern matches [a-z0-9] runs and asks the table. A word the table does not // hold is left exactly as it was, so a miss is the current behaviour rather // than a guess. package ttsnorm import ( _ "embed" "encoding/json" "log" "regexp" "strings" ) //go:embed pronounce_ru_v1.json var pronounceJSON []byte // pronounceSchemaVersion — the version this loader understands. const pronounceSchemaVersion = 1 // latinToken — one run of latin letters and digits. Cyrillic is untouched, so a // Russian word next to an English one is never rewritten by accident. var latinToken = regexp.MustCompile(`[A-Za-z][A-Za-z0-9]*`) // pronounce — lowercase word to its Russian spelling. Empty when the file // failed to load, which leaves speech exactly as it was before this existed. var pronounce = loadPronounce() func loadPronounce() map[string]string { var f struct { SchemaVersion int `json:"schema_version"` Entries map[string]string `json:"entries"` } if err := json.Unmarshal(pronounceJSON, &f); err != nil { // Speech must not stop because a dictionary is malformed. log.Printf("ttsnorm: pronunciation dictionary: %v", err) return map[string]string{} } if f.SchemaVersion != pronounceSchemaVersion { log.Printf("ttsnorm: pronunciation dictionary schema_version %d, want %d — not loading it", f.SchemaVersion, pronounceSchemaVersion) return map[string]string{} } return f.Entries } // Pronounce rewrites every latin word the dictionary knows. Case-insensitive on // the way in ("Netdata", "NETDATA" and "netdata" are one word) and lowercase on // the way out, because the value is a sound and not a name. func Pronounce(s string) string { if len(pronounce) == 0 { return s } return latinToken.ReplaceAllStringFunc(s, func(w string) string { if say, ok := pronounce[strings.ToLower(w)]; ok { return say } return w }) }