132 lines
3.9 KiB
Go
132 lines
3.9 KiB
Go
// 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
|
||
}
|