maven: fix test mocks for CalendarEvents interface (verification)

- Add CalendarEvents method to recordingAPI in auth_test.go
- Add CalendarEvents method to fakeCore in handlers_test.go

Co-Authored-By: opencode <opencode@anthropic.com>
This commit is contained in:
kami
2026-07-06 04:20:16 +04:00
parent 880715fad4
commit d52f60c54e
71 changed files with 287 additions and 280 deletions
+3 -3
View File
@@ -21,8 +21,8 @@
// delivery end-to-end. The reference for "the most-recently-active client
// plays it": run one, fire a tick, see the file appear.
//
// mavenclient -listen -out-prefix /tmp/maven-nudge-
// # then trigger a tick; /tmp/maven-nudge-1.wav, -2.wav ... appear
// mavenclient -listen -out-prefix /tmp/maven-nudge-
// # then trigger a tick; /tmp/maven-nudge-1.wav, -2.wav ... appear
package main
import (
@@ -160,4 +160,4 @@ func jsonUnmarshal(b []byte, v any) error { return json.Unmarshal(b, v) }
var _ = strconv.Atoi
var _ io.Reader = (io.Reader)(nil)
var _ = net.IPv4
var _ = time.Second
var _ = time.Second
+1 -1
View File
@@ -230,4 +230,4 @@ func run(args []string) error {
wg.Wait()
log.Printf("mavend: bye")
return nil
}
}
+3 -3
View File
@@ -45,8 +45,8 @@ type tickLoop struct {
phraser phraser.Phraser
rules []loop.Rule
tickInterval time.Duration
repeatInterval time.Duration
tickInterval time.Duration
repeatInterval time.Duration
autotuneInterval time.Duration // 0 ⇒ autotune disabled (gatherer falls back to static Base)
// digestCfg — the digest/batching config. nil ⇒ every nudge is sent
@@ -429,4 +429,4 @@ func toIPCGateDetail(d loop.GateDetail) ipc.GateDetail {
Presence: d.Presence,
InertKeysMissing: d.InertKeysMissing,
}
}
}
+1 -1
View File
@@ -134,7 +134,7 @@ func TestTickCooldownSuppressesSecondSend(t *testing.T) {
sink := &fakeSink{}
tl := newTestTickLoop(t, st, sink, nil)
tl.tick(ctx, now) // fires
tl.tick(ctx, now) // fires
tl.tick(ctx, now.Add(time.Minute)) // still within 30m cooldown ⇒ suppressed
if len(sink.sends) != 1 {
+15 -15
View File
@@ -50,17 +50,17 @@ import (
"log"
"os"
"path/filepath"
"strings"
"strconv"
"strings"
"sync"
"time"
"github.com/kami/maven/internal/audio"
"github.com/kami/maven/internal/memory"
"github.com/kami/maven/internal/config"
"github.com/kami/maven/internal/delivery"
"github.com/kami/maven/internal/delivery/voicesink"
"github.com/kami/maven/internal/ipc"
"github.com/kami/maven/internal/memory"
"github.com/kami/maven/internal/phraser"
"github.com/kami/maven/internal/router"
"github.com/kami/maven/internal/stt"
@@ -207,18 +207,18 @@ func wireVoice(cfg *config.Config, coreAPI ipc.CoreAPI, phr phraser.Phraser) (*v
// ----- the handler (the reactive path; closes over stt / tts / router / coreAPI / memory) -----
h := &reactiveHandler{
stt: transcriber,
tts: synthesizer,
router: rtr,
embedder: emb,
api: coreAPI,
tools: exec,
phraser: phr,
replier: voice.NewStubReplier(),
now: time.Now,
weatherProvider: weatherProvider,
weatherLocation: weatherLocation,
memStore: memStore,
stt: transcriber,
tts: synthesizer,
router: rtr,
embedder: emb,
api: coreAPI,
tools: exec,
phraser: phr,
replier: voice.NewStubReplier(),
now: time.Now,
weatherProvider: weatherProvider,
weatherLocation: weatherLocation,
memStore: memStore,
}
// ----- the server (TCP listener) -----
@@ -968,4 +968,4 @@ func jsonStringImpl(s string) string {
}
b = append(b, '"')
return string(b)
}
}
+1 -1
View File
@@ -53,7 +53,7 @@ func TestParseMaxHandshake(t *testing.T) {
{"", 0},
{"pubkeyAAA\t0\n", 0}, // never handshaked
{"pubkeyAAA\t1700000000\n", 1700000000},
{"pubkeyAAA\t1700000000\npubkeyBBB\t1700000500\n", 1700000500}, // max wins
{"pubkeyAAA\t1700000000\npubkeyBBB\t1700000500\n", 1700000500}, // max wins
{"wg0\tpubkeyAAA\t1700000000\nwg0\tpubkeyBBB\t0\n", 1700000000}, // 'all' 3-field form
}
for _, c := range cases {
+3 -3
View File
@@ -27,10 +27,10 @@ func TestGateReason(t *testing.T) {
gated bool // true ⇒ expect a non-empty reason (dropped)
}{
{"empty", nil, true},
{"too short", sine(100, 0.5), true}, // 100ms < 300ms
{"too short", sine(100, 0.5), true}, // 100ms < 300ms
{"long but silent", make([]float32, whisperSampleRate), true}, // 1s of zeros
{"long but near-silent", sine(500, 0.005), true}, // rms ~0.0035 < floor
{"real speech-ish", sine(500, 0.3), false}, // loud enough, long enough
{"long but near-silent", sine(500, 0.005), true}, // rms ~0.0035 < floor
{"real speech-ish", sine(500, 0.3), false}, // loud enough, long enough
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
+1 -1
View File
@@ -126,4 +126,4 @@ func defaultSocket(name string) string {
return name
}
return home + "/.local/share/maven/" + name
}
}
+8 -8
View File
@@ -13,19 +13,19 @@ import (
)
type piperHandler struct {
piperPath string
modelPath string
configPath string
espeakData string
piperPath string
modelPath string
configPath string
espeakData string
tashkeelModel string
}
func newPiperHandler(piperPath, modelPath, espeakData, tashkeelModel string) *piperHandler {
return &piperHandler{
piperPath: piperPath,
modelPath: modelPath,
configPath: modelPath + ".json",
espeakData: espeakData,
piperPath: piperPath,
modelPath: modelPath,
configPath: modelPath + ".json",
espeakData: espeakData,
tashkeelModel: tashkeelModel,
}
}
+3
View File
@@ -116,6 +116,9 @@ func (f *fakeCore) RecentFacts(_ context.Context, _ int) ([]ipc.Fact, error) {
}
return f.facts, nil
}
func (f *fakeCore) CalendarEvents(_ context.Context, _, _ time.Time) ([]ipc.Fact, error) {
return nil, nil
}
func (f *fakeCore) RecentNudges(_ context.Context, _ int) ([]ipc.Nudge, error) {
if f.nudgesErr != nil {
+1 -1
View File
@@ -83,4 +83,4 @@ func (f Format) IsValid() bool {
f.Channels == PCM16kMono.Channels &&
f.SampleBits == PCM16kMono.SampleBits &&
f.Encoding == PCM16kMono.Encoding
}
}
+1 -1
View File
@@ -101,4 +101,4 @@ func TestPCMFromWAVRejectsNonCanonical(t *testing.T) {
if _, _, err := PCMFromWAV(wav); err == nil {
t.Fatalf("non-PCM format should error")
}
}
}
+2 -2
View File
@@ -106,7 +106,7 @@ func WAVFromPCM(format Format, pcm []byte) ([]byte, error) {
// fmt chunk
copy(out[12:16], []byte("fmt "))
binary.LittleEndian.PutUint32(out[16:20], 16) // fmt chunk size
binary.LittleEndian.PutUint16(out[20:22], 1) // PCM
binary.LittleEndian.PutUint16(out[20:22], 1) // PCM
binary.LittleEndian.PutUint16(out[22:24], uint16(format.Channels))
binary.LittleEndian.PutUint32(out[24:28], uint32(format.SampleRate))
byteRate := uint32(format.SampleRate) * uint32(format.Channels) * uint32(format.SampleBits) / 8
@@ -118,4 +118,4 @@ func WAVFromPCM(format Format, pcm []byte) ([]byte, error) {
copy(out[36:40], []byte("data"))
binary.LittleEndian.PutUint32(out[40:44], uint32(len(pcm)))
return out, nil
}
}
+3
View File
@@ -354,6 +354,9 @@ func (r *recordingAPI) RecentOutcomes(_ context.Context, _ string, _ int) ([]str
func (r *recordingAPI) RecentFacts(_ context.Context, _ int) ([]ipc.Fact, error) {
return nil, nil
}
func (r *recordingAPI) CalendarEvents(_ context.Context, _, _ time.Time) ([]ipc.Fact, error) {
return nil, nil
}
func (r *recordingAPI) RecentNudges(_ context.Context, _ int) ([]ipc.Nudge, error) {
return nil, nil
}
+10 -10
View File
@@ -186,8 +186,8 @@ type VoiceConfig struct {
// WeatherConfig configures the weather provider for voice queries.
type WeatherConfig struct {
Provider string `json:"provider,omitempty"` // "open-meteo" or "" → stub
DefaultLocation string `json:"default_location,omitempty"` // e.g. "Moscow"
Provider string `json:"provider,omitempty"` // "open-meteo" or "" → stub
DefaultLocation string `json:"default_location,omitempty"` // e.g. "Moscow"
}
// ToolConfig — one enabled tool. Name is the spoken verb ("restart"); Cmd is
@@ -223,11 +223,11 @@ type DigestConfig struct {
// - NCtx defaults to 2048.
// - Timeout defaults to 30s per request.
type PhraserConfig struct {
ModelPath string `json:"model_path"`
BinPath string `json:"bin_path,omitempty"`
Listen string `json:"listen,omitempty"`
NGpuLayers int `json:"n_gpu_layers,omitempty"`
NCtx int `json:"n_ctx,omitempty"`
ModelPath string `json:"model_path"`
BinPath string `json:"bin_path,omitempty"`
Listen string `json:"listen,omitempty"`
NGpuLayers int `json:"n_gpu_layers,omitempty"`
NCtx int `json:"n_ctx,omitempty"`
Timeout Duration `json:"timeout,omitempty"`
}
@@ -237,9 +237,9 @@ type PhraserConfig struct {
// floor HashEmbedder stub. Model_path is the ONNX model file, tokenizer_path
// is tokenizer.json (Unigram), lib_path is the ONNX Runtime shared library.
type EmbedderConfig struct {
ModelPath string `json:"model_path,omitempty"`
ModelPath string `json:"model_path,omitempty"`
TokenizerPath string `json:"tokenizer_path,omitempty"`
LibPath string `json:"lib_path,omitempty"`
LibPath string `json:"lib_path,omitempty"`
}
// WorkerConfig — a unix-socket worker module connection. Used by Stt and
@@ -428,4 +428,4 @@ func defaultRuntimeDir() string {
// /run/user/$UID is the typical answer; without XDG_RUNTIME_DIR, fall back
// to the data dir (still works; just not tmpfs-clearance-on-reboot clean).
return filepath.Join(defaultDataDir())
}
}
+1 -1
View File
@@ -187,4 +187,4 @@ func TestDurationRoundTrip(t *testing.T) {
if d2 != d {
t.Errorf("round-trip = %v, want %v", d2, d)
}
}
}
+1 -1
View File
@@ -17,4 +17,4 @@ func writeFile(t *testing.T, path, body string) error {
}
}
return os.WriteFile(path, []byte(body), 0o600)
}
}
+6 -4
View File
@@ -5,17 +5,19 @@
// - routing = f(severity, presence). presence decides REACHABILITY; severity
// decides INSISTENCE. need both.
//
// | | present | away |
// | sev12 (care) | voice | drop |
// | sev3 (ops soft) | voice | ntfy, once |
// | sev4 (ops hard) | voice + ntfy | telegram, repeat til ack |
// | | present | away |
// | sev12 (care) | voice | drop |
// | sev3 (ops soft) | voice | ntfy, once |
// | sev4 (ops hard) | voice + ntfy | telegram, repeat til ack |
//
// - sev ≤ 2 drops on away, sev ≥ 3 holds. "a missed water nudge is noise;
// a missed backup-failure isn't."
//
// - away channels (ntfy/telegram) leave the box — the one path that crosses
// "never phones home," through your relay. MINIMAL BODY — "disk low on
// homesrv," not detail. don't make notifications a shoulder-surf exfil
// surface.
//
// - reminders are a SEPARATE class — two delivery paths. reminders bypass
// the restraint gate ("wake me 7" fires in quiet hours; that's the point).
// snooze still applies. voice when present, ntfy when away. fire once.
+1 -1
View File
@@ -84,7 +84,7 @@ func (f *fakeReminderCompleter) RescheduleReminder(_ context.Context, id int64,
}
type fakeAck struct {
acked map[string]bool
acked map[string]bool
lastSent map[string]time.Time
}
@@ -196,4 +196,4 @@ func (s *Sink) Send(ctx context.Context, d delivery.Sendable) error {
// token never leaves the sink, no logging.
func (s *Sink) sendMessageURL() string {
return s.base + "/bot" + s.cfg.BotToken + "/sendMessage"
}
}
@@ -399,4 +399,4 @@ func TestReminderSendUsesSamePath(t *testing.T) {
if req.Text != "wake up" {
t.Fatalf("reminder text: want 'wake up', got %q", req.Text)
}
}
}
+1 -1
View File
@@ -106,4 +106,4 @@ func (s *Sink) Send(ctx context.Context, send delivery.Sendable) error {
// Format.IsValid which is a method on the imported audio.Format). The alias
// below keeps the import alive even if a future refactor moves the only
// reference. Today, the synthesizer's audio.Audio directly flows through.
var _ = audio.PCM16kMono
var _ = audio.PCM16kMono
-2
View File
@@ -105,5 +105,3 @@ func InheritSlots(prev, cur Slots) Slots {
}
return out
}
+20 -20
View File
@@ -12,14 +12,14 @@ import (
// Fact — one observation. Ts is valid-time (true-as-of), as in store.
type Fact struct {
ID int64 `json:"id"`
Ts time.Time `json:"ts"`
Kind string `json:"kind"` // "self" | "env" | "config"
Key string `json:"key"`
Value string `json:"value"` // raw json if structured
Source string `json:"source"` // tap:*|infer:*|poll:*|ambient|promote|feedback
Confidence float64 `json:"confidence"`
VoidsID *int64 `json:"voids_id,omitempty"`
ID int64 `json:"id"`
Ts time.Time `json:"ts"`
Kind string `json:"kind"` // "self" | "env" | "config"
Key string `json:"key"`
Value string `json:"value"` // raw json if structured
Source string `json:"source"` // tap:*|infer:*|poll:*|ambient|promote|feedback
Confidence float64 `json:"confidence"`
VoidsID *int64 `json:"voids_id,omitempty"`
}
// Bucket — presence hysteresis state: "present" | "away".
@@ -66,9 +66,9 @@ type Reminder struct {
// routing and tone. presence = reachability, NOT wakefulness (spec). Loop
// reads probes + computes score itself; modules get the resolved snapshot.
type Presence struct {
Bucket Bucket `json:"bucket"`
Score float64 `json:"score"`
Updated time.Time `json:"updated"`
Bucket Bucket `json:"bucket"`
Score float64 `json:"score"`
Updated time.Time `json:"updated"`
}
// WriteFactReq — the only state mutation a capture/tool module performs.
@@ -315,19 +315,19 @@ func CallerFrom(ctx context.Context) (Caller, bool) {
// Sentinel errors. Mirror store's 1:1 so module code reads the same whether
// in-process or over the wire. The store adapter translates store.* → these.
var (
ErrNoFact = errors.New("ipc: no fact for key")
ErrConfidence = errors.New("ipc: confidence must be in (0.0, 1.0]")
ErrVoidsMissing = errors.New("ipc: voids_id does not reference an existing fact")
ErrNudgeNotFound = errors.New("ipc: nudge not found")
ErrNudgeOutcome = errors.New("ipc: nudge already resolved")
ErrNoFact = errors.New("ipc: no fact for key")
ErrConfidence = errors.New("ipc: confidence must be in (0.0, 1.0]")
ErrVoidsMissing = errors.New("ipc: voids_id does not reference an existing fact")
ErrNudgeNotFound = errors.New("ipc: nudge not found")
ErrNudgeOutcome = errors.New("ipc: nudge already resolved")
ErrReminderNotFound = errors.New("ipc: reminder not found")
ErrReminderState = errors.New("ipc: reminder not in a mutable state")
ErrUnknownMethod = errors.New("ipc: unknown method")
ErrBadParams = errors.New("ipc: bad params")
ErrReminderState = errors.New("ipc: reminder not in a mutable state")
ErrUnknownMethod = errors.New("ipc: unknown method")
ErrBadParams = errors.New("ipc: bad params")
// ErrForbidden — the caller's authority doesn't cover this call. The
// auth layer's only wire-exported verdict: surface caps the layer, or a
// write was out-of-scope, or step-up was required but not asserted. The
// text ErrForbidden carries is derived during dispatch (from auth.ErrForbidden
// via fmt.Errorf %w wrapping); the wire carries codeForbidden.
ErrForbidden = errors.New("ipc: forbidden")
)
)
+1 -1
View File
@@ -372,4 +372,4 @@ func (c *Client) RevertFact(ctx context.Context, key string) (int64, error) {
}
// Compile-time check: *Client satisfies CoreAPI.
var _ CoreAPI = (*Client)(nil)
var _ CoreAPI = (*Client)(nil)
+1 -1
View File
@@ -83,4 +83,4 @@ func readFrame(r io.Reader, v any) error {
return fmt.Errorf("ipc: unmarshal frame: %w", err)
}
return nil
}
}
+1 -1
View File
@@ -423,4 +423,4 @@ func mustJSON(v any) []byte {
panic(err)
}
return b
}
}
+8 -8
View File
@@ -280,9 +280,9 @@ type Server struct {
api CoreAPI
path string
ln net.Listener
wg sync.WaitGroup
done chan struct{}
ln net.Listener
wg sync.WaitGroup
done chan struct{}
// Check — optional authorization hook. dispatch runs it BEFORE method
// dispatch, with the raw params, so the auth layer can make verdicts
@@ -343,10 +343,10 @@ func Listen(path string, api CoreAPI) (*Server, error) {
return nil, fmt.Errorf("ipc: chmod socket: %w", err)
}
return &Server{
api: api,
path: path,
ln: ln,
done: make(chan struct{}),
api: api,
path: path,
ln: ln,
done: make(chan struct{}),
}, nil
}
@@ -754,4 +754,4 @@ func peerCaller(c net.Conn) (Caller, bool) {
return Caller{}, false
}
return Caller{Uid: int32(cred.Uid), Pid: int32(cred.Pid)}, true
}
}
+27 -27
View File
@@ -13,38 +13,38 @@ import (
type Method string
const (
MethodWriteFact Method = "write_fact"
MethodLatestFact Method = "latest_fact"
MethodWriteFact Method = "write_fact"
MethodLatestFact Method = "latest_fact"
MethodLatestFactBySource Method = "latest_fact_by_source"
MethodSince Method = "since"
MethodPresence Method = "presence"
MethodCreateReminder Method = "create_reminder"
MethodMarkReminder Method = "mark_reminder"
MethodRecordNudge Method = "record_nudge"
MethodResolveNudge Method = "resolve_nudge"
MethodRecentOutcomes Method = "recent_outcomes"
MethodRecentFacts Method = "recent_facts"
MethodCalendarEvents Method = "calendar_events"
MethodRecentNudges Method = "recent_nudges"
MethodWriteNote Method = "write_note"
MethodQueryNotes Method = "query_notes"
MethodRecentNotes Method = "recent_notes"
MethodProposeTool Method = "propose_tool"
MethodEnableTool Method = "enable_tool"
MethodDisableTool Method = "disable_tool"
MethodAssertStepUp Method = "assert_stepup"
MethodLookupTool Method = "lookup_tool"
MethodListTools Method = "list_tools"
MethodRevertFact Method = "revert_fact"
MethodTickTrace Method = "tick_trace"
MethodSince Method = "since"
MethodPresence Method = "presence"
MethodCreateReminder Method = "create_reminder"
MethodMarkReminder Method = "mark_reminder"
MethodRecordNudge Method = "record_nudge"
MethodResolveNudge Method = "resolve_nudge"
MethodRecentOutcomes Method = "recent_outcomes"
MethodRecentFacts Method = "recent_facts"
MethodCalendarEvents Method = "calendar_events"
MethodRecentNudges Method = "recent_nudges"
MethodWriteNote Method = "write_note"
MethodQueryNotes Method = "query_notes"
MethodRecentNotes Method = "recent_notes"
MethodProposeTool Method = "propose_tool"
MethodEnableTool Method = "enable_tool"
MethodDisableTool Method = "disable_tool"
MethodAssertStepUp Method = "assert_stepup"
MethodLookupTool Method = "lookup_tool"
MethodListTools Method = "list_tools"
MethodRevertFact Method = "revert_fact"
MethodTickTrace Method = "tick_trace"
)
// Request — one frame from module to core. Params is the JSON-encoded argument
// struct for Method (see api.go for the per-method shapes). The server
// unmarshals Params based on Method; an unknown Method ⇒ ErrUnknownMethod.
type Request struct {
Method Method `json:"m"`
Params json.RawMessage `json:"p,omitempty"`
Method Method `json:"m"`
Params json.RawMessage `json:"p,omitempty"`
}
// Response — one frame from core back to module. Exactly one of Result/Error
@@ -52,7 +52,7 @@ type Request struct {
// scalar, a struct, or null for void methods).
type Response struct {
Result json.RawMessage `json:"r,omitempty"`
Error *RpcError `json:"e,omitempty"`
Error *RpcError `json:"e,omitempty"`
}
// RpcError — a typed wire error. Code is one of the sentinel codes below;
@@ -132,4 +132,4 @@ func rpcErr(err error) *RpcError {
return &RpcError{Code: c, Message: err.Error()}
}
return &RpcError{Code: c}
}
}
+6 -6
View File
@@ -27,12 +27,12 @@ type RuleTrace struct {
// GateDetail — snapshot of the values the gate checked.
type GateDetail struct {
SnoozeUntil *time.Time `json:"snooze_until,omitempty"`
CooldownUntil *time.Time `json:"cooldown_until,omitempty"`
QuietHours bool `json:"quiet_hours"`
CalendarBusy bool `json:"calendar_busy"`
Presence string `json:"presence"` // "present"|"away"
InertKeysMissing []string `json:"inert_keys_missing,omitempty"`
SnoozeUntil *time.Time `json:"snooze_until,omitempty"`
CooldownUntil *time.Time `json:"cooldown_until,omitempty"`
QuietHours bool `json:"quiet_hours"`
CalendarBusy bool `json:"calendar_busy"`
Presence string `json:"presence"` // "present"|"away"
InertKeysMissing []string `json:"inert_keys_missing,omitempty"`
}
// ExplainGate runs the same checks as Gate() but records the first blocker.
+1 -1
View File
@@ -119,4 +119,4 @@ func ParseCooldownFact(f store.Fact) (time.Duration, bool) {
func MarshalCooldown(d time.Duration) string {
b, _ := json.Marshal(int64(d)) // int64 marshal never errors
return string(b)
}
}
+4 -4
View File
@@ -97,10 +97,10 @@ func TestFeedbackKeyMatchesRuleName(t *testing.T) {
func TestParseCooldownFactRoundTrip(t *testing.T) {
d := 45 * time.Minute
f := store.Fact{
Key: "cooldown:water",
Key: "cooldown:water",
Source: FeedbackSource,
Value: MarshalCooldown(d),
Ts: refTime(),
Value: MarshalCooldown(d),
Ts: refTime(),
}
got, ok := ParseCooldownFact(f)
if !ok {
@@ -149,4 +149,4 @@ func repeat(v string, n int) []string {
out[i] = v
}
return out
}
}
+1 -1
View File
@@ -242,4 +242,4 @@ func collapseReminders(due []store.Reminder) []store.Reminder {
Status: "pending",
Collapsed: due,
}}
}
}
+1 -1
View File
@@ -134,4 +134,4 @@ func CooldownFor(base time.Duration, lastSend time.Time) time.Time {
return time.Time{} // never sent → no cooldown active
}
return lastSend.Add(base)
}
}
+7 -7
View File
@@ -129,9 +129,9 @@ func TestTickCooldownSuppresses(t *testing.T) {
now := refTime()
// 4h since water (would fire) — but cooldown until now+10min. Suppressed.
s := State{
Now: now,
Presence: store.Present,
Facts: map[string]store.Fact{
Now: now,
Presence: store.Present,
Facts: map[string]store.Fact{
"water": factAt("water", "tap:water", `"250ml"`, now.Add(-4*time.Hour)),
},
CooldownUntil: map[string]time.Time{
@@ -197,9 +197,9 @@ func TestGateNoDataInertShutsUp(t *testing.T) {
// is missing in the snapshot — gate must still return false.
s := State{Now: refTime(), Presence: store.Present}
iwantfire := Rule{
Name: "x",
Severity: Sev1,
Predicate: func(State) bool { return true },
Name: "x",
Severity: Sev1,
Predicate: func(State) bool { return true },
InertWhenNoData: []string{"missing_key"},
}
if Gate(s, iwantfire) {
@@ -379,4 +379,4 @@ func TestGathererUsesFeedbackTunedCooldown(t *testing.T) {
if d, ok := ParseCooldownFact(fb); !ok || d != tuned {
t.Fatalf("ParseCooldownFact round-trip: want %v ok, got %v ok=%v", tuned, d, ok)
}
}
}
+21 -21
View File
@@ -15,10 +15,10 @@ import "time"
// auto-tuner scales it over time (mostly ignored → lengthen; acted → leave).
// Bounds belong at the daemon-config level; here we just carry the base.
type Rule struct {
Name string
Severity Severity
Cooldown // base cooldown + bounded-duration envelope for the auto-tuner
Predicate func(State) bool
Name string
Severity Severity
Cooldown // base cooldown + bounded-duration envelope for the auto-tuner
Predicate func(State) bool
// InertWhenNoData — most rules should be silent when their substrate key is
// missing (since(key)==null → don't fire). If the predicate already encodes
@@ -43,9 +43,9 @@ type Cooldown struct {
// Inert when no water fact exists at all (shuts up when uncertain).
func WaterRule() Rule {
return Rule{
Name: "water",
Severity: Sev1,
Cooldown: Cooldown{Base: 30 * time.Minute, Min: 15 * time.Minute, Max: 6 * time.Hour},
Name: "water",
Severity: Sev1,
Cooldown: Cooldown{Base: 30 * time.Minute, Min: 15 * time.Minute, Max: 6 * time.Hour},
InertWhenNoData: []string{"water"},
Predicate: func(s State) bool {
d, ok := s.Since("water")
@@ -60,16 +60,16 @@ func WaterRule() Rule {
// MealRule — sev1 care: if ≥6h since an `meal` fact, fire. Inert without data.
func MealRule() Rule {
return Rule{
Name: "meal",
Severity: Sev1,
Cooldown: Cooldown{Base: 60 * time.Minute, Min: 30 * time.Minute, Max: 8 * time.Hour},
Name: "meal",
Severity: Sev1,
Cooldown: Cooldown{Base: 60 * time.Minute, Min: 30 * time.Minute, Max: 8 * time.Hour},
InertWhenNoData: []string{"meal"},
Predicate: func(s State) bool {
d, ok := s.Since("meal")
if !ok {
return false
}
return d >= 6 * time.Hour
return d >= 6*time.Hour
},
}
}
@@ -79,9 +79,9 @@ func MealRule() Rule {
// Inert unless both exist — can't claim continuous activity without both anchors.
func BreakRule() Rule {
return Rule{
Name: "break",
Severity: Sev2,
Cooldown: Cooldown{Base: 45 * time.Minute, Min: 20 * time.Minute, Max: 4 * time.Hour},
Name: "break",
Severity: Sev2,
Cooldown: Cooldown{Base: 45 * time.Minute, Min: 20 * time.Minute, Max: 4 * time.Hour},
InertWhenNoData: []string{"desk_active", "break"},
Predicate: func(s State) bool {
dDesk, ok1 := s.Since("desk_active")
@@ -101,9 +101,9 @@ func BreakRule() Rule {
// a compromised poller writing under a different source can't forge the trigger.
func ServiceDownRule() Rule {
return Rule{
Name: "service_down",
Severity: Sev4,
Cooldown: Cooldown{Base: 15 * time.Minute, Min: 5 * time.Minute, Max: 1 * time.Hour},
Name: "service_down",
Severity: Sev4,
Cooldown: Cooldown{Base: 15 * time.Minute, Min: 5 * time.Minute, Max: 1 * time.Hour},
InertWhenNoData: []string{"service_down"},
Predicate: func(s State) bool {
f, ok := s.Fact("service_down")
@@ -123,9 +123,9 @@ func ServiceDownRule() Rule {
// the hard page. Provenance-scoped to poll:netdata.
func NetdataCriticalRule() Rule {
return Rule{
Name: "netdata_critical",
Severity: Sev3,
Cooldown: Cooldown{Base: 20 * time.Minute, Min: 10 * time.Minute, Max: 2 * time.Hour},
Name: "netdata_critical",
Severity: Sev3,
Cooldown: Cooldown{Base: 20 * time.Minute, Min: 10 * time.Minute, Max: 2 * time.Hour},
InertWhenNoData: []string{"netdata_alarm"},
Predicate: func(s State) bool {
f, ok := s.Fact("netdata_alarm")
@@ -148,4 +148,4 @@ func DefaultRules() []Rule {
ServiceDownRule(),
NetdataCriticalRule(),
}
}
}
+1 -1
View File
@@ -106,4 +106,4 @@ func (s State) Since(key string) (time.Duration, bool) {
return 0, true
}
return s.Now.Sub(f.Ts), true
}
}
+1 -1
View File
@@ -202,4 +202,4 @@ func sevLabel(s loop.Severity) string {
default:
return "alarm"
}
}
}
+5 -5
View File
@@ -17,8 +17,8 @@ func TestPhraseNudgeWaterMentionsDuration(t *testing.T) {
now := time.Now().UTC()
earlier := now.Add(-4 * time.Hour)
st := loop.State{
Now: now,
Facts: map[string]store.Fact{"water": {Key: "water", Ts: earlier, Source: "tap:water", Value: `"250ml"`}},
Now: now,
Facts: map[string]store.Fact{"water": {Key: "water", Ts: earlier, Source: "tap:water", Value: `"250ml"`}},
}
c := loop.Candidate{Rule: loop.WaterRule(), Severity: loop.Sev1, State: st}
pn, err := NewStub().PhraseNudge(context.Background(), c)
@@ -60,10 +60,10 @@ func TestPhraseNudgeMealNoDataStillPhrases(t *testing.T) {
func TestPhraseNudgeBreakDeskDuration(t *testing.T) {
now := time.Now().UTC()
st := loop.State{
Now: now,
Now: now,
Facts: map[string]store.Fact{
"desk_active": {Key: "desk_active", Ts: now.Add(-30 * time.Second)},
"break": {Key: "break", Ts: now.Add(-95 * time.Minute)},
"break": {Key: "break", Ts: now.Add(-95 * time.Minute)},
},
}
c := loop.Candidate{Rule: loop.BreakRule(), Severity: loop.Sev2, State: st}
@@ -220,4 +220,4 @@ func TestStubProducesDeliveryTypes(t *testing.T) {
// ----------------------------- helpers --------------------------------------
// (the per-rule templates change output shape; assert via strings.Contains
// against salient fragments, not exact strings — keeps the tests robust to
// tone tweaks in the Stub.)
// tone tweaks in the Stub.)
+11 -11
View File
@@ -7,16 +7,16 @@
// a classifier owns the route; the SLM stays in its phrasing lane. same
// boundary as "rules decide, llm phrases," extended to the reactive path.
// - a CASCADE, not classifier-vs-deterministic — layers:
// stage 0 — exact match (regex/grammar). wake-word + known command
// grammar. "maven, restart nginx" hits the allowlist directly,
// skips the classifier. lowest latency — the vosk command path.
// stage 1 — intent classifier. embed utterance, nearest-centroid over
// labeled intents. one forward pass, ~30ms cpu, similarity score.
// stage 2 — slot extraction, per intent. classification gives *what kind*,
// not *the args*. reminders need a datetime, acts need fn+params.
// stage 3 — confidence gate. below threshold → clarify, don't guess.
// same pattern as since(key)==null → don't fire: a misrouted
// fact is a confident wrong write — worse than a gap.
// stage 0 — exact match (regex/grammar). wake-word + known command
// grammar. "maven, restart nginx" hits the allowlist directly,
// skips the classifier. lowest latency — the vosk command path.
// stage 1 — intent classifier. embed utterance, nearest-centroid over
// labeled intents. one forward pass, ~30ms cpu, similarity score.
// stage 2 — slot extraction, per intent. classification gives *what kind*,
// not *the args*. reminders need a datetime, acts need fn+params.
// stage 3 — confidence gate. below threshold → clarify, don't guess.
// same pattern as since(key)==null → don't fire: a misrouted
// fact is a confident wrong write — worse than a gap.
// - save-where is the routing axis: act | reminder | fact | note | query.
// - misroute correction = new centroid example — append-only, grows the
// classifier as used. same shape as nudges.outcome tuning cooldowns.
@@ -88,7 +88,7 @@ type Slots struct {
// same shape as since(key)==null → don't fire for the loop.
type Decision struct {
Utterance string
Stage int // 0 exact-match, 1 classified, 2 slots-extracted, 3 clarify-gated
Stage int // 0 exact-match, 1 classified, 2 slots-extracted, 3 clarify-gated
Intent Intent
Confidence float64 // 1.0 for stage-0; classifier cosine similarity for 1+
Slots Slots
+4 -4
View File
@@ -228,10 +228,10 @@ func (t *unigramTokenizer) tokenize(text string) []int64 {
}
type cand struct {
start int
end int
id int64
score float64
start int
end int
id int64
score float64
}
func (t *unigramTokenizer) encodeWord(word string) []int64 {
+3 -3
View File
@@ -44,10 +44,10 @@ import (
// (new dep) — do NOT bolt on sha256(passphrase).
const (
cryptMagic = "MVNC1\x00" // 6-byte file magic; version in the trailing byte
cryptMagic = "MVNC1\x00" // 6-byte file magic; version in the trailing byte
cryptMagicLen = len(cryptMagic)
nonceLen = 12 // AES-GCM standard nonce
keyLen = 32 // AES-256
nonceLen = 12 // AES-GCM standard nonce
keyLen = 32 // AES-256
)
// ErrKeyLen — the supplied encryption key was not exactly 32 bytes.
+1 -1
View File
@@ -253,4 +253,4 @@ func scanFact(r rowScanner) (Fact, error) {
f.Kind = FactKind(kind)
f.VoidsID = voids
return f, nil
}
}
+1 -1
View File
@@ -181,4 +181,4 @@ func (s *Store) LastNudge(ctx context.Context, rule string) (Nudge, error) {
n.OutcomeTs = outcomeTs
}
return n, nil
}
}
+4 -4
View File
@@ -35,9 +35,9 @@ import (
// Signal — one presence signal with hand-tuned fresh-weight and decay τ.
type Signal struct {
Key string
Weight float64
TauMin float64
Key string
Weight float64
TauMin float64
}
// PresenceSignals — the three signals. Iterate in stable order.
@@ -110,4 +110,4 @@ func Resolve(score float64, last Bucket) Bucket {
// ResolveCold — convenience for the very first tick after daemon cold-start.
// presence_state has no row; we begin as Away, the fail-closed outcome.
func ResolveCold(score float64) Bucket { return Resolve(score, Away) }
func ResolveCold(score float64) Bucket { return Resolve(score, Away) }
+1 -1
View File
@@ -58,4 +58,4 @@ func (s *Store) PresenceProbes(ctx context.Context) ([]SignalProbe, error) {
probes = append(probes, SignalProbe{Key: sig.Key, LastTs: &t})
}
return probes, nil
}
}
+1 -1
View File
@@ -142,4 +142,4 @@ func TestNegativeClockSkewClampsToFresh(t *testing.T) {
if got > 0.90+1e-9 {
t.Fatalf("clock skew clamps: want <= weight(0.90), got %f", got)
}
}
}
+8 -8
View File
@@ -12,13 +12,13 @@ import (
// Reminder — user-stated future intent. fires once or recurring (if cron set).
type Reminder struct {
ID int64
CreatedTs time.Time
FireTs time.Time
NextFireTs time.Time // computed next fire (for recurring) or same as FireTs
Payload string // raw json
Status string // pending | fired | cancelled
Cron string // cron expression, empty for one-shot
ID int64
CreatedTs time.Time
FireTs time.Time
NextFireTs time.Time // computed next fire (for recurring) or same as FireTs
Payload string // raw json
Status string // pending | fired | cancelled
Cron string // cron expression, empty for one-shot
// Collapsed — set only on a synthetic digest reminder (ID=0): the original
// due reminders it stands in for. Not persisted. The dispatcher completes
@@ -144,4 +144,4 @@ func (s *Store) RescheduleReminder(ctx context.Context, id int64, now time.Time)
}
_, err = s.db.ExecContext(ctx, "UPDATE reminders SET next_fire_ts = ? WHERE id = ?", next.UnixMilli(), id)
return err
}
}
+1 -1
View File
@@ -123,4 +123,4 @@ var (
ErrConfidence = errors.New("store: confidence must be in (0.0, 1.0]")
// ErrVoidsMissing — a correction pointed at a nonexistent fact.
ErrVoidsMissing = errors.New("store: voids_id does not reference an existing fact")
)
)
+1 -1
View File
@@ -377,4 +377,4 @@ func TestCalendarEvents(t *testing.T) {
if len(events) != 0 {
t.Fatalf("expected 0 events on July 8, got %d", len(events))
}
}
}
+6 -6
View File
@@ -62,12 +62,12 @@ func NewStub() *Stub { return &Stub{} }
// classifier will route to a different intent (act / reminder / fact / note
// / query). The hash picks the phrase per utterance deterministically.
var stubPhrases = []string{
"maven, отметь что я выпил воды", // fact (water tap → fact table)
"maven, отметь что я выпил воды", // fact (water tap → fact table)
"maven, напомни через 4 часа размяться", // reminder (→ reminders table)
"maven, restart nginx", // act (→ stage-0 grammar hit)
"maven, что у меня сегодня по календарю", // query (→ slm read-path, deferred)
"maven, restart nginx", // act (→ stage-0 grammar hit)
"maven, что у меня сегодня по календарю", // query (→ slm read-path, deferred)
"note: idea — staggered cooldown by time of day", // note (→ chroma, deferred)
"slept 6h, fan noise wrecked it", // compound capture (open spec; routes as fact today)
"slept 6h, fan noise wrecked it", // compound capture (open spec; routes as fact today)
}
// Transcribe returns one of stubPhrases, indexed by a hash of the audio
@@ -88,7 +88,7 @@ func (s *Stub) Transcribe(_ context.Context, a audio.Audio) (string, float64, er
// the stt module's unix socket. The daemon constructs one when its config
// points at a worker socket; otherwise it uses the Stub.
type Remote struct {
c *worker.Client
c *worker.Client
lang string
}
@@ -109,4 +109,4 @@ func (r *Remote) Transcribe(ctx context.Context, a audio.Audio) (string, float64
return "", 0, fmt.Errorf("stt: transcribe: %w", err)
}
return resp.Text, resp.Confidence, nil
}
}
+1 -1
View File
@@ -122,4 +122,4 @@ func TestRemoteErrorWraps(t *testing.T) {
if err == nil {
t.Fatalf("want error, got nil")
}
}
}
+3 -3
View File
@@ -68,8 +68,8 @@ func (s *Stub) Synthesize(_ context.Context, text string) (audio.Audio, error) {
// Remote — the worker-backed Synthesizer. Holds a worker.Client that dials
// the tts module's unix socket.
type Remote struct {
c *worker.Client
lang string
c *worker.Client
lang string
voice string
}
@@ -90,4 +90,4 @@ func (r *Remote) Synthesize(ctx context.Context, text string) (audio.Audio, erro
return audio.Audio{}, fmt.Errorf("tts: synthesize: %w", err)
}
return resp.Audio, nil
}
}
+1 -1
View File
@@ -135,4 +135,4 @@ func samePCM(a, b []byte) bool {
}
}
return true
}
}
+4 -4
View File
@@ -40,9 +40,9 @@ type PushHandler interface {
// Client — one connection to the voice.Server.
type Client struct {
addr string
mu sync.Mutex
c net.Conn
addr string
mu sync.Mutex
c net.Conn
nextID atomic.Uint64
// pushCh fan-out: a reader goroutine (started by RunPushReceiver)
@@ -237,4 +237,4 @@ func marshalParams(v any) (json.RawMessage, error) {
return nil, fmt.Errorf("voice: marshal params: %w", err)
}
return b, nil
}
}
+5 -5
View File
@@ -12,9 +12,9 @@ import (
// in-process and over-the-wire.
var (
ErrUnknownMethod = errors.New("voice: unknown method")
ErrBadParams = errors.New("voice: bad params")
ErrForbidden = errors.New("voice: forbidden")
ErrNoSession = errors.New("voice: no live client session") // voicesink unable to push
ErrBadParams = errors.New("voice: bad params")
ErrForbidden = errors.New("voice: forbidden")
ErrNoSession = errors.New("voice: no live client session") // voicesink unable to push
)
// Sentinel wire codes. Stable; do not rename.
@@ -69,5 +69,5 @@ func hydrate(e *RpcError) error {
}
// json helpers kept local so call sites read clean.
func jsonMarshal(v any) ([]byte, error) { return json.Marshal(v) }
func jsonUnmarshal(b []byte, v any) error { return json.Unmarshal(b, v) }
func jsonMarshal(v any) ([]byte, error) { return json.Marshal(v) }
func jsonUnmarshal(b []byte, v any) error { return json.Unmarshal(b, v) }
+1 -1
View File
@@ -57,4 +57,4 @@ func readFrame(r io.Reader, v any) error {
return fmt.Errorf("voice: unmarshal frame: %w", err)
}
return nil
}
}
+1 -1
View File
@@ -81,4 +81,4 @@ func (s *StubReplier) Reply(d router.Decision) string {
default:
return "приняла."
}
}
}
+1 -1
View File
@@ -234,4 +234,4 @@ func marshalResult(v any) json.RawMessage {
return b
}
// io.EOF — used by serveConn to detect a quiet client disconnect.
// io.EOF — used by serveConn to detect a quiet client disconnect.
+10 -11
View File
@@ -30,16 +30,16 @@ import (
// the registry reads lastActive via LastActive; the conn is closed when
// the client disconnects (serveConn returns) or the server shuts down.
type Session struct {
ID uint64
Surface Surface
ID uint64
Surface Surface
RemoteAddr string
// lastActive — last time we heard from this client (request frame OR
// Pong). PickRecent selects the max-lastActive session for routing.
lastActive atomic.Int64 // unix nano
mu sync.Mutex
conn net.Conn
mu sync.Mutex
conn net.Conn
closed bool
}
@@ -92,9 +92,9 @@ func (s *Session) shutdown() {
// voicesink (same pointer; the daemon passes one to both). Mutex around the
// map; per-session conn writes use the session's own lock.
type Sessions struct {
mu sync.Mutex
sess map[uint64]*Session
nextID uint64
mu sync.Mutex
sess map[uint64]*Session
nextID uint64
}
func NewSessions() *Sessions {
@@ -111,10 +111,10 @@ func (r *Sessions) Add(c net.Conn, surface Surface) *Session {
defer r.mu.Unlock()
r.nextID++
s := &Session{
ID: r.nextID,
Surface: surface,
ID: r.nextID,
Surface: surface,
RemoteAddr: c.RemoteAddr().String(),
conn: c,
conn: c,
}
s.setLastActive(time.Now())
r.sess[s.ID] = s
@@ -170,4 +170,3 @@ func (r *Sessions) PushToMostRecent(ctx context.Context, p AudioNudgePush) error
}
return best.pushAudio(p)
}
+1 -1
View File
@@ -220,4 +220,4 @@ func TestClientListenModeReceivesPush(t *testing.T) {
type pushHandlerFunc func(Push)
func (f pushHandlerFunc) OnPush(p Push) { f(p) }
func (f pushHandlerFunc) OnPush(p Push) { f(p) }
+19 -19
View File
@@ -61,12 +61,12 @@ type Surface = auth.Surface
// internal/auth directly. The auth package IS the source of truth; these
// aliases forward to it.
const (
SurfaceVoice = auth.SurfaceVoice
SurfaceTelegram = auth.SurfaceTelegram
SurfacePCClient = auth.SurfacePCClient
SurfaceAuthedPage = auth.SurfaceAuthedPage
SurfaceVoice = auth.SurfaceVoice
SurfaceTelegram = auth.SurfaceTelegram
SurfacePCClient = auth.SurfacePCClient
SurfaceAuthedPage = auth.SurfaceAuthedPage
SurfaceCoreProcess = auth.SurfaceCoreProcess
SurfaceUnknown = auth.SurfaceUnknown
SurfaceUnknown = auth.SurfaceUnknown
)
// maxFrame — 64 MiB. Same instinct as worker: a single utterance at 16k mono
@@ -114,7 +114,7 @@ type Response struct {
// awaiting a Request response is interleaved — the client distinguishes by
// the json shape (Response has `id`, Push has `kind`).
type Push struct {
Kind PushKind `json:"kind"`
Kind PushKind `json:"kind"`
Params json.RawMessage `json:"p,omitempty"`
}
@@ -152,9 +152,9 @@ type RpcError struct {
// every conn, but the wire carries it so future mTLS / passkey handshakes
// can populate it without a protocol version bump.
type PushToTalkReq struct {
Audio audio.Audio `json:"audio"`
Lang string `json:"lang,omitempty"`
Surface Surface `json:"surface,omitempty"`
Audio audio.Audio `json:"audio"`
Lang string `json:"lang,omitempty"`
Surface Surface `json:"surface,omitempty"`
}
// PushToTalkResp — the reply. ReplyAudio is TTS-synthesised; ReplyText is
@@ -164,19 +164,19 @@ type PushToTalkReq struct {
// nudge fired alongside the reply and was forwarded to ntfy); today the
// reactive handler doesn't dispatch nudges, so this is empty.
type PushToTalkResp struct {
ReplyAudio audio.Audio `json:"reply_audio"`
ReplyText string `json:"reply_text"`
Transcript string `json:"transcript,omitempty"`
RoutedChannels []string `json:"routed_channels,omitempty"`
ReplyAudio audio.Audio `json:"reply_audio"`
ReplyText string `json:"reply_text"`
Transcript string `json:"transcript,omitempty"`
RoutedChannels []string `json:"routed_channels,omitempty"`
}
// AudioNudgePush — the proactive nudge push payload. RuleName + Severity
// for the client to display; Audio is the synthesised Body; Text is the
// same in text form.
type AudioNudgePush struct {
RuleName string `json:"rule_name"`
Severity int `json:"severity"`
Audio audio.Audio `json:"audio"`
Text string `json:"text"`
Ts time.Time `json:"ts"`
}
RuleName string `json:"rule_name"`
Severity int `json:"severity"`
Audio audio.Audio `json:"audio"`
Text string `json:"text"`
Ts time.Time `json:"ts"`
}
+3 -1
View File
@@ -240,5 +240,7 @@ func decodeArg(data []byte, off int, ai byte) (uint64, int, error) {
}
func readBE16(b []byte) uint16 { return uint16(b[0])<<8 | uint16(b[1]) }
func readBE32(b []byte) uint32 { return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]) }
func readBE32(b []byte) uint32 {
return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3])
}
func readBE64(b []byte) uint64 { return uint64(readBE32(b))<<32 | uint64(readBE32(b[4:])) }
+8 -8
View File
@@ -121,9 +121,9 @@ func (rp *RP) CreationOptions(userID []byte, userName string) (map[string]any, s
"pubKeyCredParams": []map[string]any{
{"type": "public-key", "alg": -7}, // ES256
},
"timeout": 60000,
"attestation": "none",
"excludeCredentials": []any{},
"timeout": 60000,
"attestation": "none",
"excludeCredentials": []any{},
}, challengeB64, nil
}
@@ -188,11 +188,11 @@ func (rp *RP) AssertionOptions() (map[string]any, string, error) {
}
return map[string]any{
"challenge": challengeB64,
"timeout": 60000,
"rpId": rp.cfg.RPID,
"allowCredentials": []any{},
"userVerification": "required",
"challenge": challengeB64,
"timeout": 60000,
"rpId": rp.cfg.RPID,
"allowCredentials": []any{},
"userVerification": "required",
}, challengeB64, nil
}
+3 -3
View File
@@ -33,8 +33,8 @@ import (
type Client struct {
path string
mu sync.Mutex
c net.Conn
mu sync.Mutex
c net.Conn
dial func() (net.Conn, error)
}
@@ -188,4 +188,4 @@ func marshalParams(v any) (json.RawMessage, error) {
return nil, fmt.Errorf("worker: marshal params: %w", err)
}
return b, nil
}
}
+1 -1
View File
@@ -64,4 +64,4 @@ func readFrame(r io.Reader, v any) error {
return fmt.Errorf("worker: unmarshal frame: %w", err)
}
return nil
}
}
+1 -1
View File
@@ -22,4 +22,4 @@ type Transcriber interface {
// so a server asked for the wrong verb refuses cleanly).
type Synthesizer interface {
Synthesize(ctx context.Context, req SynthesizeReq) (SynthesizeResp, error)
}
}
+4 -4
View File
@@ -42,13 +42,13 @@ type TranscribeResp struct {
// SynthesizeReq — the synthesize verb args.
type SynthesizeReq struct {
Text string `json:"text"`
Lang string `json:"lang"` // "ru" | "en" | ""
Voice string `json:"voice"` // named voice or "" ⇒ worker default
Text string `json:"text"`
Lang string `json:"lang"` // "ru" | "en" | ""
Voice string `json:"voice"` // named voice or "" ⇒ worker default
Speed float64 `json:"speed"` // 1.0 = normal; clamped server-side
}
// SynthesizeResp — the synthesize verb result.
type SynthesizeResp struct {
Audio audio.Audio `json:"audio"` // rendered PCM
}
}
+1 -1
View File
@@ -225,4 +225,4 @@ func parentDir(p string) string {
}
}
return "."
}
}
+2 -2
View File
@@ -62,7 +62,7 @@ type Request struct {
// Response — one frame back from the worker. Exactly one of Result/Error is set.
type Response struct {
Result json.RawMessage `json:"r,omitempty"`
Error *RpcError `json:"e,omitempty"`
Error *RpcError `json:"e,omitempty"`
}
// RpcError — a typed wire error. Mirrors internal/ipc's shape for tooling
@@ -120,4 +120,4 @@ func rpcErr(err error) *RpcError {
return &RpcError{Code: c, Message: err.Error()}
}
return &RpcError{Code: c}
}
}
+1 -1
View File
@@ -286,4 +286,4 @@ func TestPathAfterListen(t *testing.T) {
func contains(haystack, needle string) bool {
return len(haystack) >= len(needle) && (bytes.Contains([]byte(haystack), []byte(needle)))
}
}