Files
Maven/internal/ttsnorm/pronounce.go
T
claude b954e0cea6 a pronunciation dictionary, so piper stops reading hostnames as noise (V-458)
The RU voice reads a latin word letter by letter or guesses, so 'netdata'
came out as noise and 'homesrv' as nothing. pronounce_ru_v1.json spells the
sound in Cyrillic for the service names, hostnames and acronyms she actually
says, and Speakable applies it last, after the numbers around it are words.

Data, not code: nothing knows any of these names, and adding one is an edit
to the JSON. A word the table does not hold is left exactly as it was, so a
miss is the current behaviour rather than a guess. A malformed file logs and
loads empty, because speech must not stop over a dictionary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 01:56:11 +04:00

69 lines
2.4 KiB
Go

// 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
})
}