Luke Granger-Brown 2024-03-11 04:14:39 +00:00
parent aad93631ca
commit 6522ddba8c
11 changed files with 388 additions and 2 deletions

View file

@ -1,3 +1,6 @@
go 1.20
go 1.21.7
use ./go
use (
./go
./web/barf/sapi
)

View file

@ -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=

40
ops/nixos/totoro/barf.nix Normal file
View file

@ -0,0 +1,40 @@
# SPDX-FileCopyrightText: 2024 Luke Granger-Brown <depot@lukegb.com>
#
# 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";
};
};
}

View file

@ -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" ];

1
web/barf/README.md Normal file
View file

@ -0,0 +1 @@
# Birthday Activities Reply Frontend (BARF)

9
web/barf/default.nix Normal file
View file

@ -0,0 +1,9 @@
# SPDX-FileCopyrightText: 2024 Luke Granger-Brown <depot@lukegb.com>
#
# SPDX-License-Identifier: Apache-2.0
{ ... }@args:
{
sapi = import ./sapi args;
}

137
web/barf/sapi/default.nix Normal file
View file

@ -0,0 +1,137 @@
# SPDX-FileCopyrightText: 2024 Luke Granger-Brown <depot@lukegb.com>
#
# 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;
}

3
web/barf/sapi/go.mod Normal file
View file

@ -0,0 +1,3 @@
module hg.lukegb.com/lukegb/depot/web/barf/sapi
go 1.21

20
web/barf/sapi/mysapi4.cpp Normal file
View file

@ -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;
}
}

160
web/barf/sapi/sapidwin32.go Normal file
View file

@ -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))
}

View file

@ -23,4 +23,5 @@
'';
lukegbcom = import ./lukegbcom args;
barf = import ./barf args;
}