// Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later package tentaclewire import ( "bytes" "encoding/json" "unicode/utf8" ) // Leave room for lease metadata inside the existing 8192-byte call bound. const MaxRemoteSpeechBytes = 6144 const RemoteSpeechQueueSize = 64 const RemoteSpeechQueueBytes = 256 * 1024 // RemoteSpeechPart carries resolved character/language state for each fragment. // End is an utterance boundary, not text. Whitespace is significant. type RemoteSpeechPart struct { Text string `json:"text,omitempty"` Characters bool `json:"characters,omitempty"` Language string `json:"language,omitempty"` End bool `json:"end,omitempty"` } type RemoteSpeech struct { Kind string `json:"kind"` // speak, cancel, pause Priority int `json:"priority,omitempty"` // normal=0, next=1, now=2 Parts []RemoteSpeechPart `json:"parts,omitempty"` Paused *bool `json:"paused,omitempty"` } type RemotePresentation struct { RemoteOperation Speech RemoteSpeech `json:"speech"` Utterance uint64 `json:"utterance,omitempty"` // First fragment's operation sequence. Final bool `json:"final,omitempty"` } // A zero utterance is a complete, single-call effect. A nonzero utterance // stages speak fragments until Final; the reader admits them as one delivery. func ValidRemotePresentation(request RemotePresentation) bool { return len(request.Lease) == 32 && request.Epoch != 0 && request.Sequence != 0 && ValidRemoteSpeech(request.Speech) && (request.Utterance == 0 && !request.Final || request.Utterance > 0 && request.Utterance <= request.Sequence && request.Speech.Kind == "speak") } func (request *RemotePresentation) UnmarshalJSON(data []byte) error { type plain RemotePresentation var value plain if reject_speech_nulls(data) != nil || DecodeStrict(data, &value) != nil || !ValidRemotePresentation(RemotePresentation(value)) { return ErrProtocol } *request = RemotePresentation(value) return nil } func reject_speech_nulls(data []byte) error { var fields map[string]json.RawMessage if json.Unmarshal(data, &fields) != nil || fields == nil { return ErrProtocol } for _, value := range fields { if bytes.Equal(bytes.TrimSpace(value), []byte("null")) { return ErrProtocol } } return nil } func (part *RemoteSpeechPart) UnmarshalJSON(data []byte) error { type plain RemoteSpeechPart var value plain if reject_speech_nulls(data) != nil || DecodeStrict(data, &value) != nil { return ErrProtocol } *part = RemoteSpeechPart(value) return nil } func (speech *RemoteSpeech) UnmarshalJSON(data []byte) error { type plain RemoteSpeech var value plain if reject_speech_nulls(data) != nil || DecodeStrict(data, &value) != nil || !ValidRemoteSpeech(RemoteSpeech(value)) { return ErrProtocol } *speech = RemoteSpeech(value) return nil } // ValidRemoteSpeech also limits encoded bytes: escaped text must fit in IPC. func ValidRemoteSpeech(speech RemoteSpeech) bool { switch speech.Kind { case "speak": if speech.Priority < 0 || speech.Priority > 2 || speech.Paused != nil || len(speech.Parts) == 0 || len(speech.Parts) > 128 { return false } for _, part := range speech.Parts { if part.End { if part.Text != "" || part.Characters || part.Language != "" { return false } continue } if part.Text == "" || !utf8.ValidString(part.Text) || len(part.Language) > 64 { return false } for _, char := range part.Language { if !(char >= 'a' && char <= 'z' || char >= 'A' && char <= 'Z' || char >= '0' && char <= '9' || char == '-' || char == '_') { return false } } } case "cancel": if speech.Priority != 0 || len(speech.Parts) != 0 || speech.Paused != nil { return false } case "pause": if speech.Priority != 0 || len(speech.Parts) != 0 || speech.Paused == nil { return false } default: return false } data, err := json.Marshal(speech) return err == nil && len(data) <= MaxRemoteSpeechBytes }