cc32c2c4ab
sttSeam is modelSeam for audio and sits at the same place in wireVoice, so the voice path and the meeting recorder share one transcriber as they always have. A box with no workstation.stt block behaves byte-for-byte as it did before this existed: the floor is handed back untouched and nothing probes. An empty URL is normalised to no block at all, the way the model block already works. Health defaults to the URL's origin rather than the URL itself, because the transcribe endpoint names a path and appending would ask for /transcribe/health. A block with no token logs once that anything on the LAN can post audio to that port. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013ptwopxyo3Z2kwFckHkLvN
142 lines
5.3 KiB
Go
142 lines
5.3 KiB
Go
package config
|
|
|
|
import (
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// WorkstationConfig — the big model on the owner's desktop (workpc, a
|
|
// 7900 GRE with 16GB), fronted by mavgpud.
|
|
//
|
|
// homesrv cannot grow a GPU, so the resident Qwen3-1.7B is the floor and this
|
|
// is the preferred model above it (owner's call, 2026-08-02, docs/offload.md).
|
|
// The workstation is never assumed up: its card is often held by a CPT run and
|
|
// the machine sleeps. No block, or an empty URL, and homesrv behaves exactly as
|
|
// it does today.
|
|
//
|
|
// Only the prompt crosses the LAN, and the workstation is not "the box". The
|
|
// rules in CLAUDE.md about what may leave still apply.
|
|
type WorkstationConfig struct {
|
|
// URL — where mavgpud listens, e.g. "http://192.168.1.105:8080". Empty ⇒
|
|
// the whole block is normalised to nil and nothing probes anything.
|
|
URL string `json:"url,omitempty"`
|
|
|
|
// Health — the admission endpoint. Empty ⇒ URL + "/health", which is what
|
|
// mavgpud serves. It answers 503 while the card is held, and that is the
|
|
// signal, so it must be the supervisor's endpoint and not llama-server's.
|
|
Health string `json:"health,omitempty"`
|
|
|
|
// Probe — how often admission is re-checked. 0 ⇒ DefaultWorkstationProbe.
|
|
// Nothing on the hot path waits for it: the answer is cached and read
|
|
// atomically, so this only sets how late Maven notices the card came back.
|
|
Probe Duration `json:"probe,omitempty"`
|
|
|
|
// Timeout — the per-request budget for a completion on the workstation.
|
|
// 0 ⇒ DefaultWorkstationTimeout. A big model on a LAN host is slower than
|
|
// the resident one, and a request that overruns falls back to the floor.
|
|
Timeout Duration `json:"timeout,omitempty"`
|
|
|
|
// Stt — CrisperWhisper 2.0 on the same machine, a separate service on its
|
|
// own port. Absent ⇒ every utterance goes to mavsttd, which is today.
|
|
Stt *WorkstationSttConfig `json:"stt,omitempty"`
|
|
}
|
|
|
|
// WorkstationSttConfig — speech-to-text on the workstation.
|
|
//
|
|
// It is a second service and not a second endpoint on mavgpud: whisper.cpp
|
|
// cannot load CrisperWhisper 2.0 at all, because it derives its language count
|
|
// from the vocabulary size and CW2's 51897 tokens shift seven special token
|
|
// ids. So CW2 runs under transformers, and this block addresses it.
|
|
//
|
|
// Worth the trouble: CW2 turbo scores 10.4% WER in Russian against 27.5% for
|
|
// the ggml-small.bin homesrv loads
|
|
// (docs/evals/2026-08-09-crisperwhisper2-russian-wer.md).
|
|
type WorkstationSttConfig struct {
|
|
// URL — the transcribe endpoint, e.g.
|
|
// "http://192.168.1.105:8081/transcribe". Empty ⇒ the block is normalised
|
|
// to nil and mavsttd takes every turn.
|
|
URL string `json:"url,omitempty"`
|
|
|
|
// Health — the admission endpoint. Empty ⇒ the URL's origin + "/health".
|
|
// It answers 503 while the card is held, and that is the signal.
|
|
Health string `json:"health,omitempty"`
|
|
|
|
// Token — the bearer token the service checks. Audio is the most sensitive
|
|
// thing that crosses this seam, so a LAN deployment should set one. Write
|
|
// it as ${MAVEN_STT_TOKEN} and keep the value in deploy/telegram.env, the
|
|
// way every other secret in this file is written.
|
|
Token string `json:"token,omitempty"`
|
|
|
|
// Probe — how often admission is re-checked. 0 ⇒ DefaultWorkstationProbe.
|
|
Probe Duration `json:"probe,omitempty"`
|
|
|
|
// Timeout — the per-request budget for one utterance. 0 ⇒
|
|
// DefaultWorkstationSttTimeout. A request that overruns falls back to
|
|
// mavsttd, which costs a worse transcript and not the turn.
|
|
Timeout Duration `json:"timeout,omitempty"`
|
|
}
|
|
|
|
// Workstation defaults, applied in normaliseWorkstation.
|
|
const (
|
|
DefaultWorkstationProbe = 15 * time.Second
|
|
DefaultWorkstationTimeout = 90 * time.Second
|
|
// One utterance, not one completion. A voice turn waits on this, so the
|
|
// budget is a few seconds and not a minute and a half.
|
|
DefaultWorkstationSttTimeout = 10 * time.Second
|
|
)
|
|
|
|
// normaliseWorkstation applies the block's defaults. No address, no preferred
|
|
// model: an unconfigured workstation is the default deploy and must be
|
|
// indistinguishable from today.
|
|
func (c *Config) normaliseWorkstation() {
|
|
if c.Workstation != nil && strings.TrimSpace(c.Workstation.URL) == "" {
|
|
c.Workstation = nil
|
|
}
|
|
if c.Workstation == nil {
|
|
return
|
|
}
|
|
w := c.Workstation
|
|
if strings.TrimSpace(w.Health) == "" {
|
|
w.Health = strings.TrimRight(w.URL, "/") + "/health"
|
|
}
|
|
if w.Probe <= 0 {
|
|
w.Probe = Duration(DefaultWorkstationProbe)
|
|
}
|
|
if w.Timeout <= 0 {
|
|
w.Timeout = Duration(DefaultWorkstationTimeout)
|
|
}
|
|
normaliseWorkstationStt(w)
|
|
}
|
|
|
|
// normaliseWorkstationStt applies the speech-to-text block's defaults. No
|
|
// address, no remote: mavsttd then takes every utterance, which is today.
|
|
func normaliseWorkstationStt(w *WorkstationConfig) {
|
|
if w.Stt != nil && strings.TrimSpace(w.Stt.URL) == "" {
|
|
w.Stt = nil
|
|
}
|
|
if w.Stt == nil {
|
|
return
|
|
}
|
|
s := w.Stt
|
|
if strings.TrimSpace(s.Health) == "" {
|
|
s.Health = healthOrigin(s.URL)
|
|
}
|
|
if s.Probe <= 0 {
|
|
s.Probe = Duration(DefaultWorkstationProbe)
|
|
}
|
|
if s.Timeout <= 0 {
|
|
s.Timeout = Duration(DefaultWorkstationSttTimeout)
|
|
}
|
|
}
|
|
|
|
// healthOrigin derives the admission endpoint from the transcribe endpoint.
|
|
// The URL names a path, so appending to it would ask for /transcribe/health.
|
|
func healthOrigin(raw string) string {
|
|
u, err := url.Parse(raw)
|
|
if err != nil || u.Host == "" {
|
|
return strings.TrimRight(raw, "/") + "/health"
|
|
}
|
|
return u.Scheme + "://" + u.Host + "/health"
|
|
}
|