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 { stdin.Close() return worker.SynthesizeResp{}, fmt.Errorf("piper: stdout pipe: %w", err) } if err := cmd.Start(); err != nil { stdin.Close() stdout.Close() return worker.SynthesizeResp{}, fmt.Errorf("piper: start: %w", err) } if _, err := io.WriteString(stdin, req.Text); err != nil { stdin.Close() stdout.Close() _ = cmd.Wait() return worker.SynthesizeResp{}, fmt.Errorf("piper: write text: %w", err) } stdin.Close() rawPCM, readErr := io.ReadAll(stdout) stdout.Close() 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 }