initial commit
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
// Package worker is the audio-job boundary between core and its stt/tts
|
||||
// modules.
|
||||
//
|
||||
// It is a SIBLING to internal/ipc, not a reuse of it:
|
||||
//
|
||||
// - internal/ipc is the core↔module *state* boundary. Modules call INTO
|
||||
// core to write facts / read presence. core is the server; the module
|
||||
// is the client. The verbs (WriteFact, Since, ...) are authority-bound;
|
||||
// the auth layer scopes them per Caller.
|
||||
// - internal/worker is the core↔module *job* boundary. Core calls OUT to
|
||||
// the stt/tts module: "transcribe these bytes", "synthesize this text."
|
||||
// Core is the client here; the worker module is the server. The verbs
|
||||
// are not authority-bound — they are pure compute over the payload core
|
||||
// already owns (audio bytes / reply text). Nothing about authority moves
|
||||
// across this boundary because the worker has no key, no facts, nothing
|
||||
// stateful to read or write.
|
||||
//
|
||||
// The reversal matters: a module that ships audio work to core would have
|
||||
// to hold a Caller identity, an enrolled daemon surface, etc.; core that
|
||||
// ships work to a module ships only the work. Same unix-socket floor (0600
|
||||
// dir + socket perms carry "same user") but the flow is reversed, and the
|
||||
// surface stays out of the auth cascade — the worker never reaches into
|
||||
// core's state, so it has nothing to be capped against.
|
||||
//
|
||||
// Transport: unix domain socket, local-only, same-host as core. Same wire
|
||||
// shape as ipc (4-byte big-endian length + JSON body) so `socat`/`nc` can
|
||||
// debug it identically. Frame cap is 64 MiB (vs ipc's 4) so audio blobs fit
|
||||
// — a minute of 16k mono int16 is ~1.9 MiB, comfortably under; an hour's
|
||||
// sleep-clip transcription is implausible to send as one frame but the cap
|
||||
// permits long-enough live clips without framing the worker into chunks.
|
||||
package worker
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// maxFrame — 64 MiB. Audio up to ~3 minutes @ 16k mono int16 fits one frame
|
||||
// with headroom; longer audio is chunked by the caller (meeting-record mode
|
||||
// is post-MVP) or rejected with ErrFrameTooLarge. Sized for stt inputs and
|
||||
// tts outputs at single-utterance scale.
|
||||
const maxFrame = 64 << 20
|
||||
|
||||
// Method — one RPC verb. Adding one is a worker-API change, not an authority
|
||||
// change (worker has no authority surface — see package doc). The two verbs
|
||||
// mirror the stt/tts interfaces the daemon wires; new verbs go with a new
|
||||
// module kind (e.g. translation), not a new feature of an existing one.
|
||||
type Method string
|
||||
|
||||
const (
|
||||
MethodTranscribe Method = "transcribe"
|
||||
MethodSynthesize Method = "synthesize"
|
||||
)
|
||||
|
||||
// Request — one frame from core to a worker module.
|
||||
type Request struct {
|
||||
Method Method `json:"m"`
|
||||
Params json.RawMessage `json:"p,omitempty"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// RpcError — a typed wire error. Mirrors internal/ipc's shape for tooling
|
||||
// parity (same args to socat, same `errors.Is` rehydration pattern); worker
|
||||
// sentinels are independent since this boundary is independent.
|
||||
type RpcError struct {
|
||||
Code string `json:"c"`
|
||||
Message string `json:"m,omitempty"`
|
||||
}
|
||||
|
||||
func (e *RpcError) Error() string {
|
||||
if e.Message != "" {
|
||||
return fmt.Sprintf("worker: %s: %s", e.Code, e.Message)
|
||||
}
|
||||
return fmt.Sprintf("worker: %s", e.Code)
|
||||
}
|
||||
|
||||
// Sentinel codes. Stable over the wire — do not rename. Mirror the package
|
||||
// sentinels 1:1.
|
||||
const (
|
||||
codeUnknownMethod = "unknown_method"
|
||||
codeBadParams = "bad_params"
|
||||
codeInternal = "internal"
|
||||
)
|
||||
|
||||
// Sentinel errors. The client rehydrates a wire RpcError into one of these so
|
||||
// callers use errors.Is the same way they would in-process.
|
||||
var (
|
||||
ErrUnknownMethod = errors.New("worker: unknown method")
|
||||
ErrBadParams = errors.New("worker: bad params")
|
||||
)
|
||||
|
||||
// codeOf maps a server-side sentinel to its wire code. Anything not matched
|
||||
// is codeInternal — internal Go error text never ships to the caller; the
|
||||
// server logs the real text and the client sees a generic code.
|
||||
func codeOf(err error) string {
|
||||
switch {
|
||||
case err == nil:
|
||||
return ""
|
||||
case errors.Is(err, ErrUnknownMethod):
|
||||
return codeUnknownMethod
|
||||
case errors.Is(err, ErrBadParams):
|
||||
return codeBadParams
|
||||
default:
|
||||
return codeInternal
|
||||
}
|
||||
}
|
||||
|
||||
// rpcErr builds the wire error for a server-side error. message is omitted
|
||||
// for sentinel codes (the Code carries the meaning). internal errors carry
|
||||
// the message text — it's not authority-bearing text, just a diagnostic.
|
||||
func rpcErr(err error) *RpcError {
|
||||
c := codeOf(err)
|
||||
if c == codeInternal || c == codeBadParams {
|
||||
return &RpcError{Code: c, Message: err.Error()}
|
||||
}
|
||||
return &RpcError{Code: c}
|
||||
}
|
||||
Reference in New Issue
Block a user