fix zombie leak, add quiet-hours toggle, improve query reply, configurable router threshold, JS dashboard
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
// Package main is mavsttd — maven's stt module process.
|
||||
//
|
||||
// Per spec: stt/tts are restart-free, key-free, fail-independent modules —
|
||||
// separate processes from core, reachable over the worker boundary
|
||||
// (internal/worker). Core dials mavsttd's unix socket and ships audio bytes
|
||||
// for transcription; mavsttd ships text back.
|
||||
//
|
||||
// With -model <path>: loads a whisper.cpp ggml model (e.g. ggml-small.bin)
|
||||
// for real transcription. Without -model: serves the stub transcriber
|
||||
// (deterministic no-model floor) so the loop is exercisable end-to-end
|
||||
// without weights.
|
||||
//
|
||||
// Module topology:
|
||||
//
|
||||
// $ mavsttd -socket /run/user/$UID/maven/stt.sock
|
||||
//
|
||||
// The daemon's config points at this socket:
|
||||
//
|
||||
// "stt": { "socket": "/run/user/1000/maven/stt.sock" }
|
||||
//
|
||||
// Both processes are same-user on the box ⇒ the 0600 socket floor (same
|
||||
// unix user) is sufficient today; the wg / mTLS cuts in internal/auth are
|
||||
// for the NETWORK radius (client↔core), not the local module radius.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/kami/maven/internal/worker"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := run(os.Args[1:]); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "mavsttd:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run(args []string) error {
|
||||
sock := flag.String("socket", defaultSocket("stt.sock"), "unix socket path")
|
||||
model := flag.String("model", "", "path to whisper ggml model file")
|
||||
flag.CommandLine.Parse(args)
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||
defer stop()
|
||||
|
||||
var t worker.Transcriber
|
||||
if *model != "" {
|
||||
w, err := newWhisperHandler(*model)
|
||||
if err != nil {
|
||||
return fmt.Errorf("whisper: %w", err)
|
||||
}
|
||||
t = w
|
||||
defer func() {
|
||||
log.Printf("mavsttd: closing whisper model")
|
||||
w.Close()
|
||||
}()
|
||||
log.Printf("mavsttd: loaded whisper model from %s", *model)
|
||||
} else {
|
||||
log.Printf("mavsttd: no model specified, using stub handler")
|
||||
t = &stubHandler{}
|
||||
}
|
||||
|
||||
srv := worker.NewServer(*sock, t)
|
||||
if err := srv.Listen(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer srv.Close()
|
||||
log.Printf("mavsttd: worker listening on %s", srv.Path())
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() { errCh <- srv.Serve() }()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Printf("mavsttd: shutdown signal received")
|
||||
srv.Close()
|
||||
return nil
|
||||
case err := <-errCh:
|
||||
if err != nil && !errors.Is(err, net.ErrClosed) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// stubHandler — worker.Transcriber that delegates to the package Stub. Tiny
|
||||
// now; the production swap replaces this whole struct with a faster-whisper
|
||||
// / vosk-backed struct (the same Worker.Transcriber interface).
|
||||
type stubHandler struct{}
|
||||
|
||||
func (h *stubHandler) Transcribe(ctx context.Context, req worker.TranscribeReq) (worker.TranscribeResp, error) {
|
||||
// delegate to the same deterministic Stub the daemon could have wired
|
||||
// in-process; mavsttd is the "separate process" equivalent.
|
||||
_ = req
|
||||
// hash for variation; same approach as stt.Stub.
|
||||
if len(req.Audio.Bytes) == 0 {
|
||||
return worker.TranscribeResp{Text: "maven, что у меня сегодня", Confidence: 1.0}, nil
|
||||
}
|
||||
// vary phrase by first byte for visibility in logs/tests.
|
||||
phrases := []string{
|
||||
"maven, отметь что я выпил воды",
|
||||
"maven, напомни через 4 часа размяться",
|
||||
"maven, restart nginx",
|
||||
"maven, что у меня сегодня по календарю",
|
||||
"note: staggered cooldown by time of day",
|
||||
"slept 6h",
|
||||
}
|
||||
idx := int(req.Audio.Bytes[0]) % len(phrases)
|
||||
return worker.TranscribeResp{Text: phrases[idx], Confidence: 1.0}, nil
|
||||
}
|
||||
|
||||
// defaultSocket returns XDG_RUNTIME_DIR/maven/<name> if set, falling back
|
||||
// to a homedir-relative path (mirrors config.defaultRuntimeDir).
|
||||
func defaultSocket(name string) string {
|
||||
if x := os.Getenv("XDG_RUNTIME_DIR"); x != "" {
|
||||
return x + "/maven/" + name
|
||||
}
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil || home == "" {
|
||||
return name
|
||||
}
|
||||
return home + "/.local/share/maven/" + name
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -I${SRCDIR}/../../deps/include -I${SRCDIR}/../../deps/whisper.cpp/ggml/include
|
||||
#cgo LDFLAGS: -L${SRCDIR}/../../deps/lib -Wl,-rpath,${SRCDIR}/../../deps/lib -lwhisper -lggml -lggml-base -lggml-cpu -lggml-vulkan -lm -lstdc++ -fopenmp
|
||||
#include <whisper.h>
|
||||
#include <stdlib.h>
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"unsafe"
|
||||
|
||||
"github.com/kami/maven/internal/worker"
|
||||
)
|
||||
|
||||
type whisperHandler struct {
|
||||
ctx *C.struct_whisper_context
|
||||
}
|
||||
|
||||
func newWhisperHandler(modelPath string) (*whisperHandler, error) {
|
||||
cparams := C.whisper_context_default_params()
|
||||
cPath := C.CString(modelPath)
|
||||
defer C.free(unsafe.Pointer(cPath))
|
||||
|
||||
ctx := C.whisper_init_from_file_with_params(cPath, cparams)
|
||||
if ctx == nil {
|
||||
return nil, fmt.Errorf("whisper: failed to init from %s", modelPath)
|
||||
}
|
||||
return &whisperHandler{ctx: ctx}, nil
|
||||
}
|
||||
|
||||
func (h *whisperHandler) Transcribe(ctx context.Context, req worker.TranscribeReq) (worker.TranscribeResp, error) {
|
||||
a := req.Audio
|
||||
if len(a.Bytes) == 0 {
|
||||
return worker.TranscribeResp{}, fmt.Errorf("whisper: empty audio")
|
||||
}
|
||||
|
||||
nSamples := len(a.Bytes) / 2
|
||||
samples := make([]float32, nSamples)
|
||||
for i := 0; i < nSamples; i++ {
|
||||
s := int16(a.Bytes[i*2]) | int16(a.Bytes[i*2+1])<<8
|
||||
samples[i] = float32(s) / 32768.0
|
||||
}
|
||||
|
||||
params := C.whisper_full_default_params(C.WHISPER_SAMPLING_GREEDY)
|
||||
params.print_progress = false
|
||||
params.print_realtime = false
|
||||
params.print_timestamps = false
|
||||
params.print_special = false
|
||||
params.n_threads = C.int(4)
|
||||
params.single_segment = true
|
||||
|
||||
lang := C.CString(req.Lang)
|
||||
defer C.free(unsafe.Pointer(lang))
|
||||
params.language = lang
|
||||
params.detect_language = false
|
||||
|
||||
cSamples := (*C.float)(unsafe.Pointer(&samples[0]))
|
||||
res := C.whisper_full(h.ctx, params, cSamples, C.int(nSamples))
|
||||
if res != 0 {
|
||||
return worker.TranscribeResp{}, fmt.Errorf("whisper: full failed: %d", int(res))
|
||||
}
|
||||
|
||||
nSegments := int(C.whisper_full_n_segments(h.ctx))
|
||||
if nSegments == 0 {
|
||||
return worker.TranscribeResp{Text: "", Confidence: 0}, nil
|
||||
}
|
||||
|
||||
var text string
|
||||
totalLogProb := float64(0)
|
||||
totalTokens := 0
|
||||
|
||||
for i := 0; i < nSegments; i++ {
|
||||
cSeg := C.whisper_full_get_segment_text(h.ctx, C.int(i))
|
||||
if cSeg != nil {
|
||||
text += C.GoString(cSeg)
|
||||
}
|
||||
|
||||
nTokens := int(C.whisper_full_n_tokens(h.ctx, C.int(i)))
|
||||
for j := 0; j < nTokens; j++ {
|
||||
p := float64(C.whisper_full_get_token_p(h.ctx, C.int(i), C.int(j)))
|
||||
if p > 0 {
|
||||
totalLogProb += math.Log(p)
|
||||
totalTokens++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
confidence := 0.0
|
||||
if totalTokens > 0 {
|
||||
avgLogProb := totalLogProb / float64(totalTokens)
|
||||
confidence = math.Exp(avgLogProb)
|
||||
}
|
||||
|
||||
noSpeechProb := float64(C.whisper_full_get_segment_no_speech_prob(h.ctx, 0))
|
||||
if noSpeechProb > 0.9 {
|
||||
confidence = 0
|
||||
}
|
||||
|
||||
if math.IsNaN(confidence) || math.IsInf(confidence, 0) {
|
||||
confidence = 0
|
||||
}
|
||||
|
||||
return worker.TranscribeResp{
|
||||
Text: text,
|
||||
Confidence: confidence,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *whisperHandler) Close() {
|
||||
if h.ctx != nil {
|
||||
C.whisper_free(h.ctx)
|
||||
h.ctx = nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user