Luke Granger-Brown
6522ddba8c
sapi sapi sapi http://totoro:11316/sam?text=OLE%20Apartments%20are%20Very%20Complicated%20and%20cannot%20be%20understood%20by%20Mere%20Mortals.
160 lines
3.6 KiB
Go
160 lines
3.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io/fs"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"unsafe"
|
|
)
|
|
|
|
var (
|
|
serve = flag.String("serve", ":11316", "Port number.")
|
|
cacheDir = flag.String("cache_dir", "c:\\temp\\sapid-cache", "Cache dir.")
|
|
|
|
sapi = syscall.NewLazyDLL("sapi4.dll")
|
|
procMakeSamSay = sapi.NewProc("MakeSamSay")
|
|
|
|
sayitMutex sync.Mutex
|
|
)
|
|
|
|
type sayResponse struct {
|
|
Bytes []byte
|
|
Err error
|
|
}
|
|
|
|
type sayRequest struct {
|
|
Text string
|
|
ResponseCh chan sayResponse
|
|
}
|
|
|
|
func sayRoutine(reqCh chan sayRequest) {
|
|
//defer os.Exit(1)
|
|
//defer log.Printf("sayRoutine is exiting!!!")
|
|
runtime.LockOSThread()
|
|
for req := range reqCh {
|
|
respBytes, err := say(req.Text)
|
|
req.ResponseCh <- sayResponse{respBytes, err}
|
|
}
|
|
}
|
|
|
|
var (
|
|
sayChan chan sayRequest
|
|
)
|
|
|
|
func say(str string) ([]byte, error) {
|
|
td, err := os.MkdirTemp("", "sayit-sapi")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer os.RemoveAll(td)
|
|
|
|
if err := os.Chdir(td); err != nil {
|
|
return nil, fmt.Errorf("chdir %v: %w", td, err)
|
|
}
|
|
|
|
inp := make([]byte, len(str)+1)
|
|
copy(inp, str)
|
|
inpP := &inp[0]
|
|
|
|
outfile := make([]byte, 17)
|
|
outfileP := &outfile[0]
|
|
outfilePP := &outfileP
|
|
r1, _, _ := syscall.SyscallN(procMakeSamSay.Addr(), uintptr(unsafe.Pointer(inpP)), uintptr(unsafe.Pointer(outfilePP)))
|
|
if r1 != 1 {
|
|
return nil, fmt.Errorf("MakeSamSay failed")
|
|
}
|
|
|
|
wavPath := path.Join(td, strings.TrimSpace(string(outfile[:16])))
|
|
log.Printf("input %q: reading from %v", str, wavPath)
|
|
return os.ReadFile(wavPath)
|
|
}
|
|
|
|
func sayUncached(ctx context.Context, str string) ([]byte, error) {
|
|
respCh := make(chan sayResponse, 1)
|
|
sayChan <- sayRequest{Text: str, ResponseCh: respCh}
|
|
resp := <-respCh
|
|
if resp.Err != nil {
|
|
return nil, resp.Err
|
|
}
|
|
return resp.Bytes, nil
|
|
}
|
|
|
|
func sayCached(ctx context.Context, str string) ([]byte, error) {
|
|
str = strings.TrimSpace(str)
|
|
hb := sha256.Sum256([]byte(str))
|
|
hbHex := hex.EncodeToString(hb[:])
|
|
|
|
cachedStr, err := os.ReadFile(path.Join(*cacheDir, hbHex+".txt"))
|
|
if err == nil {
|
|
if strings.TrimSpace(string(cachedStr)) == str {
|
|
return os.ReadFile(path.Join(*cacheDir, hbHex+".wav"))
|
|
}
|
|
} else if !errors.Is(err, fs.ErrNotExist) {
|
|
return nil, fmt.Errorf("checking cache %v/%v: %w", *cacheDir, hbHex, err)
|
|
}
|
|
|
|
out, err := sayUncached(ctx, str)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := os.WriteFile(path.Join(*cacheDir, hbHex+".txt"), []byte(str), 0644); err != nil {
|
|
log.Printf("failed writing cache key to %v/%v.txt: %w", *cacheDir, hbHex, err)
|
|
return out, nil
|
|
}
|
|
if err := os.WriteFile(path.Join(*cacheDir, hbHex+".wav"), out, 0644); err != nil {
|
|
log.Printf("failed writing cache data to %v/%v.wav: %w", *cacheDir, hbHex, err)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func makeNoise(rw http.ResponseWriter, r *http.Request) {
|
|
text := r.FormValue("text")
|
|
if text == "" {
|
|
http.Error(rw, "bad request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
sayFunc := sayCached
|
|
if *cacheDir == "" {
|
|
sayFunc = sayUncached
|
|
}
|
|
respBytes, err := sayFunc(r.Context(), text)
|
|
if err != nil {
|
|
log.Printf("rendering %q: %v", text, err)
|
|
http.Error(rw, "failed rendering", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
rw.Header().Set("Content-type", "audio/x-wav")
|
|
rw.Write(respBytes)
|
|
}
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
|
|
if *cacheDir != "" {
|
|
if err := os.MkdirAll(*cacheDir, 0700); err != nil {
|
|
log.Fatalf("creating cache dir %v: %v", *cacheDir, err)
|
|
}
|
|
}
|
|
|
|
sayChan = make(chan sayRequest)
|
|
go sayRoutine(sayChan)
|
|
|
|
http.HandleFunc("/sam", makeNoise)
|
|
log.Printf("Listening on %s", *serve)
|
|
log.Fatal(http.ListenAndServe(*serve, nil))
|
|
}
|