fix zombie leak, add quiet-hours toggle, improve query reply, configurable router threshold, JS dashboard
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
// 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 <binary> -model <onnx>: 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
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
|
||||
"github.com/kami/maven/internal/audio"
|
||||
"github.com/kami/maven/internal/worker"
|
||||
)
|
||||
|
||||
type piperHandler struct {
|
||||
piperPath string
|
||||
modelPath string
|
||||
configPath string
|
||||
espeakData string
|
||||
tashkeelModel string
|
||||
}
|
||||
|
||||
func newPiperHandler(piperPath, modelPath, espeakData, tashkeelModel string) *piperHandler {
|
||||
return &piperHandler{
|
||||
piperPath: piperPath,
|
||||
modelPath: modelPath,
|
||||
configPath: modelPath + ".json",
|
||||
espeakData: espeakData,
|
||||
tashkeelModel: tashkeelModel,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *piperHandler) Synthesize(ctx context.Context, req worker.SynthesizeReq) (worker.SynthesizeResp, error) {
|
||||
var stderr bytes.Buffer
|
||||
|
||||
args := []string{
|
||||
"--model", h.modelPath,
|
||||
"--config", h.configPath,
|
||||
"--output_raw",
|
||||
"--quiet",
|
||||
}
|
||||
if h.espeakData != "" {
|
||||
args = append(args, "--espeak_data", h.espeakData)
|
||||
}
|
||||
if h.tashkeelModel != "" {
|
||||
args = append(args, "--tashkeel_model", h.tashkeelModel)
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, h.piperPath, args...)
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return worker.SynthesizeResp{}, fmt.Errorf("piper: stdin pipe: %w", err)
|
||||
}
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return worker.SynthesizeResp{}, fmt.Errorf("piper: stdout pipe: %w", err)
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return worker.SynthesizeResp{}, fmt.Errorf("piper: start: %w", err)
|
||||
}
|
||||
|
||||
_, _ = io.WriteString(stdin, req.Text)
|
||||
stdin.Close()
|
||||
|
||||
rawPCM, readErr := io.ReadAll(stdout)
|
||||
|
||||
waitErr := cmd.Wait()
|
||||
if waitErr != nil {
|
||||
errMsg := stderr.String()
|
||||
if errMsg != "" {
|
||||
return worker.SynthesizeResp{}, fmt.Errorf("piper: %s: %s", waitErr, errMsg)
|
||||
}
|
||||
return worker.SynthesizeResp{}, fmt.Errorf("piper: %w", waitErr)
|
||||
}
|
||||
if readErr != nil {
|
||||
return worker.SynthesizeResp{}, fmt.Errorf("piper: read stdout: %w", readErr)
|
||||
}
|
||||
|
||||
if len(rawPCM) == 0 {
|
||||
return worker.SynthesizeResp{}, fmt.Errorf("piper: no audio output")
|
||||
}
|
||||
|
||||
resampled := resample22050To16000(rawPCM)
|
||||
|
||||
return worker.SynthesizeResp{
|
||||
Audio: audio.Audio{
|
||||
Format: audio.PCM16kMono,
|
||||
Bytes: resampled,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// resample22050To16000 converts raw 16-bit PCM from 22050 Hz to 16000 Hz
|
||||
// using linear interpolation.
|
||||
func resample22050To16000(input []byte) []byte {
|
||||
if len(input) < 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
nSamples := len(input) / 2
|
||||
outSamples := int(float64(nSamples) * 16000.0 / 22050.0)
|
||||
output := make([]byte, outSamples*2)
|
||||
|
||||
ratio := 22050.0 / 16000.0
|
||||
|
||||
for i := 0; i < outSamples; i++ {
|
||||
srcPos := float64(i) * ratio
|
||||
srcIdx := int(srcPos)
|
||||
frac := srcPos - float64(srcIdx)
|
||||
|
||||
if srcIdx >= nSamples-1 {
|
||||
v := int16(binary.LittleEndian.Uint16(input[(nSamples-1)*2:]))
|
||||
binary.LittleEndian.PutUint16(output[i*2:], uint16(v))
|
||||
continue
|
||||
}
|
||||
|
||||
v0 := int16(binary.LittleEndian.Uint16(input[srcIdx*2:]))
|
||||
v1 := int16(binary.LittleEndian.Uint16(input[(srcIdx+1)*2:]))
|
||||
|
||||
interpolated := int16(float64(v0)*(1-frac) + float64(v1)*frac)
|
||||
binary.LittleEndian.PutUint16(output[i*2:], uint16(interpolated))
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
Reference in New Issue
Block a user