rudder-engine builds for macOS and Windows, finds its runtime per platform, and accepts the desktop webview's stream
The engine did not compile outside Linux: Pdeathsig (kill children with the engine) is Linux-only. It now sits behind a build tag, with a no-op elsewhere where the desktop shell owns the engine's lifetime. The default speech-server path names this platform's NeMo-Speech.cpp release folder instead of the Linux one. The /stream WebSocket accepts the Tauri origins (tauri://localhost, http://tauri.localhost) and RUDDER_ALLOW_ORIGIN as well as the page's own host, and still refuses any other site. Cross-compiled for darwin/arm64, darwin/amd64 and windows/amd64.
This commit is contained in:
parent
7080546fdf
commit
ea151b9a56
4 changed files with 57 additions and 7 deletions
|
|
@ -24,7 +24,6 @@ import (
|
|||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
|
@ -215,7 +214,7 @@ func (w *deviceWatcher) run() {
|
|||
for {
|
||||
cmd := exec.Command("pactl", "subscribe")
|
||||
cmd.Env = audioEnv()
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Pdeathsig: syscall.SIGKILL}
|
||||
cmd.SysProcAttr = childAttr()
|
||||
out, err := cmd.StdoutPipe()
|
||||
if err == nil && cmd.Start() == nil {
|
||||
sc := bufio.NewScanner(out)
|
||||
|
|
@ -255,7 +254,7 @@ func (e *engine) micTest(device string) event {
|
|||
return event{"ok": false, "verdict": "No recorder found (need parec, pw-record or arecord)."}
|
||||
}
|
||||
cmd.Env = audioEnv()
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Pdeathsig: syscall.SIGKILL}
|
||||
cmd.SysProcAttr = childAttr()
|
||||
var errBuf strings.Builder
|
||||
cmd.Stderr = &errBuf
|
||||
out, err := cmd.StdoutPipe()
|
||||
|
|
|
|||
|
|
@ -29,11 +29,13 @@ import (
|
|||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
|
@ -61,6 +63,21 @@ func envOr(k, d string) string {
|
|||
return d
|
||||
}
|
||||
|
||||
// defaultNemoBin: where setup puts NVIDIA's NeMo-Speech.cpp 0.1.0 CPU release for THIS platform — the
|
||||
// release folders are named nemo-speech-0.1.0-<linux|macos|windows>-<x86_64|aarch64>-cpu.
|
||||
func defaultNemoBin(base string) string {
|
||||
osName := map[string]string{"darwin": "macos"}[runtime.GOOS]
|
||||
if osName == "" {
|
||||
osName = runtime.GOOS
|
||||
}
|
||||
arch := map[string]string{"amd64": "x86_64", "arm64": "aarch64"}[runtime.GOARCH]
|
||||
exe := "nemo-speech"
|
||||
if runtime.GOOS == "windows" {
|
||||
exe += ".exe"
|
||||
}
|
||||
return filepath.Join(base, "bin", "nemo-speech-0.1.0-"+osName+"-"+arch+"-cpu", "bin", exe)
|
||||
}
|
||||
|
||||
func loadConfig() config {
|
||||
home, _ := os.UserHomeDir()
|
||||
// RUDDER_HOME is where setup-runtime.sh installed the runtime and model (a server keeps it on a volume).
|
||||
|
|
@ -68,7 +85,7 @@ func loadConfig() config {
|
|||
return config{
|
||||
Port: envOr("RUDDER_PORT", "5193"),
|
||||
NemoPort: envOr("RUDDER_NEMO_PORT", "5194"),
|
||||
NemoBin: envOr("RUDDER_NEMO_BIN", filepath.Join(base, "bin", "nemo-speech-0.1.0-linux-x86_64-cpu", "bin", "nemo-speech")),
|
||||
NemoBin: envOr("RUDDER_NEMO_BIN", defaultNemoBin(base)),
|
||||
Model: envOr("RUDDER_MODEL", filepath.Join(base, "models", "nemotron-3.5-asr-streaming-0.6b.q8_0.gguf")),
|
||||
Language: envOr("RUDDER_LANG", "en-US"),
|
||||
EndpointMs: 800,
|
||||
|
|
@ -207,7 +224,7 @@ func (e *engine) startNemo() {
|
|||
cmd := exec.Command(e.cfg.NemoBin, "serve",
|
||||
"--host", "127.0.0.1", "--port", e.cfg.NemoPort,
|
||||
"--asr-model", e.cfg.Model, "--device", "cpu", "--no-ui", "--api-key", e.nemoKey)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Pdeathsig: syscall.SIGKILL} // dies with us
|
||||
cmd.SysProcAttr = childAttr() // dies with us
|
||||
out, _ := cmd.StdoutPipe()
|
||||
cmd.Stderr = cmd.Stdout
|
||||
if err := cmd.Start(); err != nil {
|
||||
|
|
@ -669,7 +686,7 @@ func spawnRecorder(device string, logs *ring) (*recorderProc, error) {
|
|||
}
|
||||
cmd := exec.Command(c[0], c[1:]...)
|
||||
cmd.Env = audioEnv()
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Pdeathsig: syscall.SIGKILL}
|
||||
cmd.SysProcAttr = childAttr()
|
||||
errBuf := &bytes.Buffer{}
|
||||
cmd.Stderr = errBuf
|
||||
out, err := cmd.StdoutPipe()
|
||||
|
|
@ -813,7 +830,17 @@ func main() {
|
|||
// /api/rudder/stream (RUDDER_AUDIO=browser): a WebSocket of binary 16 kHz mono s16le PCM frames from the
|
||||
// page's microphone. Opening it starts a session, closing it stops one; the text comes back on /events.
|
||||
// The upgrader's default origin check (Origin must match Host) keeps other sites out.
|
||||
upgrader := websocket.Upgrader{ReadBufferSize: 8192}
|
||||
// Origin: the page's own host (the dev server proxy), a desktop app's webview (tauri://localhost, or
|
||||
// http://tauri.localhost on Windows — the same origins guard() answers CORS for), or RUDDER_ALLOW_ORIGIN.
|
||||
// Anything else is another site and is refused.
|
||||
upgrader := websocket.Upgrader{ReadBufferSize: 8192, CheckOrigin: func(r *http.Request) bool {
|
||||
o := r.Header.Get("Origin")
|
||||
if o == "" || o == "tauri://localhost" || o == "http://tauri.localhost" || (os.Getenv("RUDDER_ALLOW_ORIGIN") != "" && o == os.Getenv("RUDDER_ALLOW_ORIGIN")) {
|
||||
return true
|
||||
}
|
||||
u, err := url.Parse(o)
|
||||
return err == nil && strings.EqualFold(u.Host, r.Host)
|
||||
}}
|
||||
mux.HandleFunc("/api/rudder/stream", func(w http.ResponseWriter, r *http.Request) {
|
||||
if !browserAudio() {
|
||||
writeJSON(w, 409, event{"error": "this engine records a local microphone (RUDDER_AUDIO is not browser)"})
|
||||
|
|
|
|||
12
backend/rudder-engine/procattr_linux.go
Normal file
12
backend/rudder-engine/procattr_linux.go
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
//go:build linux
|
||||
|
||||
package main
|
||||
|
||||
import "syscall"
|
||||
|
||||
// childAttr: a child process (the speech server, a recorder) is killed with the engine, so a crash
|
||||
// never leaves a speech server holding a gigabyte of memory. Linux only: Pdeathsig does not exist on
|
||||
// macOS or Windows (see procattr_other.go).
|
||||
func childAttr() *syscall.SysProcAttr {
|
||||
return &syscall.SysProcAttr{Pdeathsig: syscall.SIGKILL}
|
||||
}
|
||||
12
backend/rudder-engine/procattr_other.go
Normal file
12
backend/rudder-engine/procattr_other.go
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
//go:build !linux
|
||||
|
||||
package main
|
||||
|
||||
import "syscall"
|
||||
|
||||
// childAttr: macOS and Windows have no parent-death signal. In the desktop app the Tauri shell owns the
|
||||
// engine's lifetime and the engine kills its children on a normal exit (shutdown); a crashed engine can
|
||||
// leave its speech server running until the app is next closed.
|
||||
func childAttr() *syscall.SysProcAttr {
|
||||
return nil
|
||||
}
|
||||
Loading…
Reference in a new issue