From 6522ddba8cf510d23af0c9e6e24d3b1cc7b13b94 Mon Sep 17 00:00:00 2001 From: Luke Granger-Brown Date: Mon, 11 Mar 2024 04:14:39 +0000 Subject: [PATCH] web/barf: init, including sapi sapi sapi sapi http://totoro:11316/sam?text=OLE%20Apartments%20are%20Very%20Complicated%20and%20cannot%20be%20understood%20by%20Mere%20Mortals. --- go.work | 7 +- go.work.sum | 11 +++ ops/nixos/totoro/barf.nix | 40 +++++++++ ops/nixos/totoro/default.nix | 1 + web/barf/README.md | 1 + web/barf/default.nix | 9 ++ web/barf/sapi/default.nix | 137 ++++++++++++++++++++++++++++++ web/barf/sapi/go.mod | 3 + web/barf/sapi/mysapi4.cpp | 20 +++++ web/barf/sapi/sapidwin32.go | 160 +++++++++++++++++++++++++++++++++++ web/default.nix | 1 + 11 files changed, 388 insertions(+), 2 deletions(-) create mode 100644 ops/nixos/totoro/barf.nix create mode 100644 web/barf/README.md create mode 100644 web/barf/default.nix create mode 100644 web/barf/sapi/default.nix create mode 100644 web/barf/sapi/go.mod create mode 100644 web/barf/sapi/mysapi4.cpp create mode 100644 web/barf/sapi/sapidwin32.go diff --git a/go.work b/go.work index 09dd41b74a..bf3a037c5d 100644 --- a/go.work +++ b/go.work @@ -1,3 +1,6 @@ -go 1.20 +go 1.21.7 -use ./go +use ( + ./go + ./web/barf/sapi +) diff --git a/go.work.sum b/go.work.sum index 4efa1e92da..bb2b71e10d 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,4 +1,15 @@ +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest/autorest v0.11.20 h1:s8H1PbCZSqg/DH7JMlOz6YMig6htWLNPsjDdlLqCx3M= +github.com/Azure/go-autorest/autorest/adal v0.9.15 h1:X+p2GF0GWyOiSmqohIaEeuNFNDY4I4EOlVuUQvFdWMk= +github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= +github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= +github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c= +github.com/golang-jwt/jwt/v4 v4.0.0 h1:RAqyYixv1p7uEnocuy8P1nru5wprCh/MH2BIlW5z5/o= github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/ops/nixos/totoro/barf.nix b/ops/nixos/totoro/barf.nix new file mode 100644 index 0000000000..a17e823dcd --- /dev/null +++ b/ops/nixos/totoro/barf.nix @@ -0,0 +1,40 @@ +# SPDX-FileCopyrightText: 2024 Luke Granger-Brown +# +# SPDX-License-Identifier: Apache-2.0 + +{ depot, pkgs, ... }: + +{ + systemd.targets.barf = { + wantedBy = [ "multi-user.target" ]; + }; + systemd.services.barf-sapid = { + wantedBy = [ "barf.target" ]; + serviceConfig = { + ExecStart = "${depot.web.barf.sapi.sapid-wrapper}/bin/sapid-wrapper"; + CacheDirectory = "barf-sapid"; + User = "barf-sapid"; + KillMode = "mixed"; + PrivateTmp = true; + PrivateDevices = true; + RestrictNamespaces = true; + RestrictRealtime = true; + ProtectKernelLogs = true; + ProtectControlGroups = true; + ProtectHostname = true; + ProtectHome = true; + ProtectProc = "invisible"; + ProcSubset = "pid"; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectClock = true; + CapabilityBoundingSet = ""; + LockPersonality = true; + PrivateUsers = true; + RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6 AF_NETLINK"; + + DynamicUser = true; + Restart = "always"; + }; + }; +} diff --git a/ops/nixos/totoro/default.nix b/ops/nixos/totoro/default.nix index 0d35335e69..8621649caa 100644 --- a/ops/nixos/totoro/default.nix +++ b/ops/nixos/totoro/default.nix @@ -21,6 +21,7 @@ in { ./home-assistant.nix ./authentik.nix ./adsb.nix + ./barf.nix ]; boot.initrd.availableKernelModules = [ "xhci_pci" "ahci" "nvme" "usb_storage" "usbhid" "sd_mod" ]; diff --git a/web/barf/README.md b/web/barf/README.md new file mode 100644 index 0000000000..3423dc5edb --- /dev/null +++ b/web/barf/README.md @@ -0,0 +1 @@ +# Birthday Activities Reply Frontend (BARF) diff --git a/web/barf/default.nix b/web/barf/default.nix new file mode 100644 index 0000000000..4dd2818984 --- /dev/null +++ b/web/barf/default.nix @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2024 Luke Granger-Brown +# +# SPDX-License-Identifier: Apache-2.0 + +{ ... }@args: + +{ + sapi = import ./sapi args; +} diff --git a/web/barf/sapi/default.nix b/web/barf/sapi/default.nix new file mode 100644 index 0000000000..e99625451b --- /dev/null +++ b/web/barf/sapi/default.nix @@ -0,0 +1,137 @@ +# SPDX-FileCopyrightText: 2024 Luke Granger-Brown +# +# SPDX-License-Identifier: Apache-2.0 + +{ pkgs, lib, ... }: + +let + spchapi = pkgs.fetchurl { + url = "https://www.htmlbible.com/MicrosoftSpeechComponents/SAPI4point0aRuntimeBinaries/spchapi.exe"; + hash = "sha256:1pb3wcv80g0kk3q94rfr29spxa8asd8qzrc05yg57hkv441ywmc9"; + }; + tv_enua = pkgs.fetchurl { + url = "https://www.htmlbible.com/MicrosoftSpeechComponents/TextToSpeechEngines/AmericanEnglish/tv_enua.exe"; + hash = "sha256:0cckcpmndnq9d0r0vmpch5pmw76lgzxn3983196niysi5683d6bh"; + }; + msam = pkgs.fetchurl { + url = "https://archive.org/download/Sam_mike_and_mary/microsoft_sam%2C_mike%2C_and_mary.exe"; + hash = "sha256:0pixa50v5lyhv7i6d8p2b6lb2pf38gr4lmym9v5m1ljkkr49lzy8"; + }; + + wineprefix = pkgs.runCommand "sapi-wineprefix" { + nativeBuildInputs = [ pkgs.wine pkgs.xvfb-run ]; + stampVersion = "1"; + } '' + mkdir $out + echo "$stampVersion" > $out/version + mkdir $out/home + export HOME=$out/home + export WINEPREFIX=$out/wineprefix + xvfb-run -a wine ${spchapi} || true + xvfb-run -a wine ${tv_enua} /q || true + xvfb-run -a wine ${msam} /q || true + xvfb-run -a wine ${sapi4src}/SAPI4SDK.exe /q || true + ''; + + sapi4src = pkgs.fetchFromGitHub { + owner = "TETYYS"; + repo = "SAPI4"; + rev = "23b0dd17083cc2fd7d5656d2996640a90cb58ea0"; + hash = "sha256:1yjjfhbs6d29vyvi2bs96mhr0zmjlyf8m8snrxiq0c5z5f4imfzb"; + }; + w32pkgs = pkgs.pkgsCross.mingw32; + w32BuildPackages = w32pkgs.buildPackages; + gcc = w32BuildPackages.wrapCC (w32BuildPackages.gcc-unwrapped.override ({ + threadsCross = { + model = "win32"; + package = null; + }; + })); + stdenv' = w32pkgs.overrideCC w32pkgs.stdenv gcc; + sapi4 = stdenv'.mkDerivation { + pname = "sapi4"; + version = "0.0.1"; + src = sapi4src; + + buildPhase = '' + export CC=i686-w64-mingw32-g++ + export MSSPEECH="${wineprefix}/wineprefix/drive_c/Program Files/Microsoft Speech SDK/Include/" + + cat ${./mysapi4.cpp} >> sapi4.cpp + + echo sapi4 + "$CC" -shared -Wl,--out-implib,sapi4.lib -o sapi4.dll sapi4.cpp -I "$MSSPEECH" -lole32 -luser32 -luuid + # echo sapi4limits + # "$CC" -o sapi4limits.exe sapi4limits.cpp -I "$MSSPEECH" -lole32 -luser32 -luuid -L. -lsapi4 + # echo sapi4out + # "$CC" -o sapi4out.exe sapi4out.cpp -I "$MSSPEECH" -lole32 -luser32 -luuid -L. -lsapi4 + + mkdir -p $out/bin + cp sapi4.dll $out/bin + ''; + }; + sayit = pkgs.writeShellApplication { + name = "sayit-sapi"; + + runtimeInputs = [ pkgs.wine ]; + + text = '' + prefixStampVersion="0" + export WINEPREFIXBASE="''${CACHE_DIRECTORY:-$HOME/.cache}/sayit-sapi-wineprefix" + if [[ -f "$WINEPREFIXBASE/version" ]]; then + prefixStampVersion="$(cat "$WINEPREFIXBASE/version")" + fi + if [[ "$prefixStampVersion" != "${wineprefix.stampVersion}" ]]; then + rm -rf "$WINEPREFIXBASE" + cp -R "${wineprefix}" "$WINEPREFIXBASE" + chmod -R u+w "$WINEPREFIXBASE" + fi + + export WINEPREFIX="$WINEPREFIXBASE/wineprefix" + export WINEDEBUG="''${WINEDEBUG:--all}" + exec wine ${sapi4}/bin/sapi4out.exe Sam 100 150 "$1" + ''; + }; + sapid = pkgs.buildGoModule { + name = "sapid"; + src = lib.sourceByRegex ./. [".*\.go$" "go.mod"]; + vendorHash = null; + + postInstall = '' + for f in ${sapi4}/bin/*.dll; do + ln -s "$f" $out/bin + done + mv $out/bin/windows_386/*.exe $out/bin + rmdir $out/bin/windows_386 + ''; + + preBuild = '' + export GOOS=windows GOARCH=386 + ''; + }; + sapid-wrapper = pkgs.writeShellApplication { + name = "sapid-wrapper"; + + runtimeInputs = [ pkgs.xvfb-run pkgs.wine ]; + + text = '' + prefixStampVersion="0" + export WINEPREFIXBASE="''${CACHE_DIRECTORY:-$HOME/.cache}/sayit-sapi-wineprefix" + if [[ -f "$WINEPREFIXBASE/version" ]]; then + prefixStampVersion="$(cat "$WINEPREFIXBASE/version")" + fi + if [[ "$prefixStampVersion" != "${wineprefix.stampVersion}" ]]; then + rm -rf "$WINEPREFIXBASE" + cp -R "${wineprefix}" "$WINEPREFIXBASE" + chmod -R u+w "$WINEPREFIXBASE" + fi + + export WINEPREFIX="$WINEPREFIXBASE/wineprefix" + export WINEDEBUG="''${WINEDEBUG:--all}" + exec xvfb-run -a wine "${sapid}/bin/sapi.exe" "$@" + ''; + }; +in + { + inherit spchapi tv_enua msam wineprefix sapi4src sapi4 sayit sapid sapid-wrapper; + } diff --git a/web/barf/sapi/go.mod b/web/barf/sapi/go.mod new file mode 100644 index 0000000000..36f5501592 --- /dev/null +++ b/web/barf/sapi/go.mod @@ -0,0 +1,3 @@ +module hg.lukegb.com/lukegb/depot/web/barf/sapi + +go 1.21 diff --git a/web/barf/sapi/mysapi4.cpp b/web/barf/sapi/mysapi4.cpp new file mode 100644 index 0000000000..396f0c839a --- /dev/null +++ b/web/barf/sapi/mysapi4.cpp @@ -0,0 +1,20 @@ +extern "C" { + +extern __declspec(dllexport) BOOL MakeSamSay(LPCSTR text, LPSTR* OutFile) { + VOICE_INFO voiceInfo; + bool ret = false; + if (!InitializeForVoice("Sam", &voiceInfo)) { + goto end; + } + + UINT64 len; + if (GetTTS(&voiceInfo, 100, 150, text, &len, OutFile)) { + ret = true; + } + + DeinitializeForVoice(&voiceInfo); +end: + return ret; +} + +} diff --git a/web/barf/sapi/sapidwin32.go b/web/barf/sapi/sapidwin32.go new file mode 100644 index 0000000000..6fc40254df --- /dev/null +++ b/web/barf/sapi/sapidwin32.go @@ -0,0 +1,160 @@ +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)) +} diff --git a/web/default.nix b/web/default.nix index 004bb23a96..2c3a1e87f8 100644 --- a/web/default.nix +++ b/web/default.nix @@ -23,4 +23,5 @@ ''; lukegbcom = import ./lukegbcom args; + barf = import ./barf args; }