@frockbot/protocol 0.3.16 → 0.3.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/voice-assistant.ts +155 -0
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/** Browser ↔ VoiceSession protocol for the app-wide assistant. */
|
|
2
|
+
export const VOICE_ASSISTANT_VERSION_V1 = 1 as const;
|
|
3
|
+
export const VOICE_ASSISTANT_INPUT_SAMPLE_RATE_V1 = 16_000;
|
|
4
|
+
export const VOICE_ASSISTANT_OUTPUT_SAMPLE_RATE_V1 = 24_000;
|
|
5
|
+
|
|
6
|
+
export type VoiceAssistantLiveStateV1 =
|
|
7
|
+
"connecting" | "listening" | "speaking" | "offline";
|
|
8
|
+
|
|
9
|
+
/** Audio is a binary PCM16LE frame; these are the browser's text controls. */
|
|
10
|
+
export type VoiceAssistantClientFrameV1 = {
|
|
11
|
+
schemaVersion: 1;
|
|
12
|
+
type: "stop";
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type VoiceAssistantServerFrameV1 =
|
|
16
|
+
| {
|
|
17
|
+
schemaVersion: 1;
|
|
18
|
+
type: "ready";
|
|
19
|
+
sessionId: string;
|
|
20
|
+
quotaRemainingSeconds: number;
|
|
21
|
+
}
|
|
22
|
+
| {
|
|
23
|
+
schemaVersion: 1;
|
|
24
|
+
type: "state";
|
|
25
|
+
state: "listening" | "speaking";
|
|
26
|
+
}
|
|
27
|
+
| {
|
|
28
|
+
schemaVersion: 1;
|
|
29
|
+
type: "transcript";
|
|
30
|
+
id: string;
|
|
31
|
+
speaker: "user" | "assistant";
|
|
32
|
+
text: string;
|
|
33
|
+
at: string;
|
|
34
|
+
}
|
|
35
|
+
| {
|
|
36
|
+
schemaVersion: 1;
|
|
37
|
+
type: "tool";
|
|
38
|
+
id: string;
|
|
39
|
+
name: string;
|
|
40
|
+
label: string;
|
|
41
|
+
at: string;
|
|
42
|
+
}
|
|
43
|
+
| { schemaVersion: 1; type: "interrupted" }
|
|
44
|
+
| {
|
|
45
|
+
schemaVersion: 1;
|
|
46
|
+
type: "offline";
|
|
47
|
+
reason: "stopped" | "idle" | "quota" | "error" | "replaced";
|
|
48
|
+
message: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function object(input: unknown): Record<string, unknown> {
|
|
52
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
53
|
+
throw new Error("voice assistant frame must be an object");
|
|
54
|
+
}
|
|
55
|
+
const value = input as Record<string, unknown>;
|
|
56
|
+
if (value.schemaVersion !== 1) {
|
|
57
|
+
throw new Error("voice assistant frame version is unsupported");
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function text(
|
|
63
|
+
value: Record<string, unknown>,
|
|
64
|
+
field: string,
|
|
65
|
+
max = 8_192,
|
|
66
|
+
): string {
|
|
67
|
+
const found = value[field];
|
|
68
|
+
if (typeof found !== "string" || !found || found.length > max) {
|
|
69
|
+
throw new Error(`voice assistant frame.${field} is invalid`);
|
|
70
|
+
}
|
|
71
|
+
return found;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function decodeVoiceAssistantClientFrameV1(
|
|
75
|
+
input: unknown,
|
|
76
|
+
): VoiceAssistantClientFrameV1 {
|
|
77
|
+
const value = object(input);
|
|
78
|
+
if (value.type !== "stop") {
|
|
79
|
+
throw new Error("voice assistant client frame type is invalid");
|
|
80
|
+
}
|
|
81
|
+
return { schemaVersion: 1, type: "stop" };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function decodeVoiceAssistantServerFrameV1(
|
|
85
|
+
input: unknown,
|
|
86
|
+
): VoiceAssistantServerFrameV1 {
|
|
87
|
+
const value = object(input);
|
|
88
|
+
switch (value.type) {
|
|
89
|
+
case "ready": {
|
|
90
|
+
if (
|
|
91
|
+
!Number.isSafeInteger(value.quotaRemainingSeconds) ||
|
|
92
|
+
(value.quotaRemainingSeconds as number) < 0
|
|
93
|
+
) {
|
|
94
|
+
throw new Error("voice assistant frame quota is invalid");
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
schemaVersion: 1,
|
|
98
|
+
type: "ready",
|
|
99
|
+
sessionId: text(value, "sessionId", 128),
|
|
100
|
+
quotaRemainingSeconds: value.quotaRemainingSeconds as number,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
case "state":
|
|
104
|
+
if (value.state !== "listening" && value.state !== "speaking") {
|
|
105
|
+
throw new Error("voice assistant frame state is invalid");
|
|
106
|
+
}
|
|
107
|
+
return { schemaVersion: 1, type: "state", state: value.state };
|
|
108
|
+
case "transcript":
|
|
109
|
+
if (value.speaker !== "user" && value.speaker !== "assistant") {
|
|
110
|
+
throw new Error("voice assistant frame speaker is invalid");
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
schemaVersion: 1,
|
|
114
|
+
type: "transcript",
|
|
115
|
+
id: text(value, "id", 128),
|
|
116
|
+
speaker: value.speaker,
|
|
117
|
+
text: text(value, "text"),
|
|
118
|
+
at: text(value, "at", 64),
|
|
119
|
+
};
|
|
120
|
+
case "tool":
|
|
121
|
+
return {
|
|
122
|
+
schemaVersion: 1,
|
|
123
|
+
type: "tool",
|
|
124
|
+
id: text(value, "id", 128),
|
|
125
|
+
name: text(value, "name", 128),
|
|
126
|
+
label: text(value, "label", 160),
|
|
127
|
+
at: text(value, "at", 64),
|
|
128
|
+
};
|
|
129
|
+
case "interrupted":
|
|
130
|
+
return { schemaVersion: 1, type: "interrupted" };
|
|
131
|
+
case "offline": {
|
|
132
|
+
const reasons = ["stopped", "idle", "quota", "error", "replaced"];
|
|
133
|
+
if (!reasons.includes(String(value.reason))) {
|
|
134
|
+
throw new Error("voice assistant frame reason is invalid");
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
schemaVersion: 1,
|
|
138
|
+
type: "offline",
|
|
139
|
+
reason: value.reason as Extract<
|
|
140
|
+
VoiceAssistantServerFrameV1,
|
|
141
|
+
{ type: "offline" }
|
|
142
|
+
>["reason"],
|
|
143
|
+
message: text(value, "message", 1_024),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
default:
|
|
147
|
+
throw new Error("voice assistant server frame type is invalid");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function parseVoiceAssistantServerFrameV1(
|
|
152
|
+
input: string,
|
|
153
|
+
): VoiceAssistantServerFrameV1 {
|
|
154
|
+
return decodeVoiceAssistantServerFrameV1(JSON.parse(input) as unknown);
|
|
155
|
+
}
|