// Package main is mavttsd — maven's tts module process. // // Sibling to cmd/mavsttd: same worker boundary, opposite job (synthesize vs // transcribe). Same restart-free, key-free, fail-independent invariant. // // With -piper -model : calls piper for real TTS (ru_RU // voice at models/tts/ru_RU-irina-medium.onnx). Without flags: serves the // stub synthesizer (200ms tone) for exercisable end-to-end testing. // // $ mavttsd -socket /run/user/$UID/maven/tts.sock // "tts": { "socket": "/run/user/1000/maven/tts.sock", "lang": "ru" } package main import ( "context" "errors" "flag" "fmt" "log" "net" "os" "os/signal" "syscall" "github.com/kami/maven/internal/audio" "github.com/kami/maven/internal/worker" ) func main() { if err := run(os.Args[1:]); err != nil { fmt.Fprintln(os.Stderr, "mavttsd:", err) os.Exit(1) } } func run(args []string) error { sock := flag.String("socket", defaultSocket("tts.sock"), "unix socket path") piperBin := flag.String("piper", "", "path to piper binary") model := flag.String("model", "", "path to piper onnx model file") espeakData := flag.String("espeak_data", "", "path to espeak-ng data directory") tashkeelModel := flag.String("tashkeel_model", "", "path to libtashkeel onnx model") flag.CommandLine.Parse(args) ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) defer stop() var s worker.Synthesizer if *piperBin != "" && *model != "" { s = newPiperHandler(*piperBin, *model, *espeakData, *tashkeelModel) log.Printf("mavttsd: using piper tts (%s, model=%s)", *piperBin, *model) } else { log.Printf("mavttsd: no piper/model specified, using stub handler") s = &stubHandler{} } srv := worker.NewSynthesizerServer(*sock, s) if err := srv.Listen(); err != nil { return err } defer srv.Close() log.Printf("mavttsd: worker listening on %s", srv.Path()) errCh := make(chan error, 1) go func() { errCh <- srv.Serve() }() select { case <-ctx.Done(): log.Printf("mavttsd: shutdown signal received") srv.Close() return nil case err := <-errCh: if err != nil && !errors.Is(err, net.ErrClosed) { return err } return nil } } // stubHandler — worker.Synthesizer that delegates to the tts Stub. The // production swap replaces this struct with a silero / piper-backed handler. type stubHandler struct{} func (h *stubHandler) Synthesize(ctx context.Context, req worker.SynthesizeReq) (worker.SynthesizeResp, error) { _ = ctx // 200ms tone, freq keyed by first byte of text — same shape as tts.Stub, // kept locally so this module has zero coupling to the daemon package // (mavttsd running shouldn't drag stt/tts package symbols here; they're // siblings in the topology). const samples = 3200 // 200ms @ 16k pcm := make([]byte, samples*2) freq := 220.0 if len(req.Text) > 0 { freq = 180.0 + float64(req.Text[0]%6)*60 } for i := 0; i < samples; i++ { t := float64(i) / 16000.0 v := int16(12000 * sin(2*pi*freq*t)) pcm[i*2] = byte(v) pcm[i*2+1] = byte(v >> 8) } return worker.SynthesizeResp{Audio: audio.Audio{Format: audio.PCM16kMono, Bytes: pcm}}, nil } const pi = 3.141592653589793 // tiny stdlib-free sin approximation — keeps mavttsd out of math import. // Adequate for a tone generator; the production model returns real audio. func sin(x float64) float64 { // reduce to [-pi, +pi] mod := x - pi*2*float64(int(x/(pi*2))) if mod > pi { mod -= pi * 2 } else if mod < -pi { mod += pi * 2 } // 4-term Taylor series around 0; decent for the small amplitudes here. return mod - mod*mod*mod/6 + mod*mod*mod*mod*mod/120 - mod*mod*mod*mod*mod*mod*mod/5040 } func defaultSocket(name string) string { if x := os.Getenv("XDG_RUNTIME_DIR"); x != "" { return x + "/maven/" + name } home, err := os.UserHomeDir() if err != nil || home == "" { return name } return home + "/.local/share/maven/" + name }