// 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 : 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") minMs := flag.Int("min-ms", 300, "drop clips shorter than this (silence gate); whisper hallucinates on short/quiet audio") silenceRMS := flag.Float64("silence-rms", 0.01, "drop clips with normalized RMS below this (silence gate); tune to your mic floor") 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, *minMs, *silenceRMS) 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/ 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 }