@voicethere/agent 0.8.3 → 0.9.0

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.
@@ -178,6 +178,12 @@ function isSttControlAckMessage(value) {
178
178
  const msg = value;
179
179
  return msg.type === "stt_control_ack" && typeof msg.requestId === "string";
180
180
  }
181
+ function isVoiceLanguageControlAckMessage(value) {
182
+ if (!value || typeof value !== "object")
183
+ return false;
184
+ const msg = value;
185
+ return msg.type === "voice_language_control_ack" && typeof msg.requestId === "string";
186
+ }
181
187
  function isWebhookMessage(value) {
182
188
  if (!value || typeof value !== "object")
183
189
  return false;
@@ -220,7 +226,7 @@ function isParentMessage(value) {
220
226
  if (!value || typeof value !== "object")
221
227
  return false;
222
228
  const msg = value;
223
- return msg.type === "session_start" || msg.type === "speech_event" || msg.type === "session_end" || msg.type === "data_channel_message" || msg.type === "data_channel_binary" || msg.type === "idle_timeout" || msg.type === "recording_control_ack" || msg.type === "conversation_history_control_ack" || msg.type === "play_ack" || msg.type === "play_status_ack" || msg.type === "play_stop_ack" || msg.type === "play_pose_ack" || msg.type === "mix_control_ack" || msg.type === "stt_control_ack" || msg.type === "webhook";
229
+ return msg.type === "session_start" || msg.type === "speech_event" || msg.type === "session_end" || msg.type === "data_channel_message" || msg.type === "data_channel_binary" || msg.type === "idle_timeout" || msg.type === "recording_control_ack" || msg.type === "conversation_history_control_ack" || msg.type === "play_ack" || msg.type === "play_status_ack" || msg.type === "play_stop_ack" || msg.type === "play_pose_ack" || msg.type === "mix_control_ack" || msg.type === "stt_control_ack" || msg.type === "voice_language_control_ack" || msg.type === "webhook";
224
230
  }
225
231
  function isSessionScopedParentMessage(value) {
226
232
  return isParentMessage(value) && !isWebhookMessage(value);
@@ -247,6 +253,7 @@ var pendingPlayStopAcks = /* @__PURE__ */ new Map();
247
253
  var pendingPlayPoseAcks = /* @__PURE__ */ new Map();
248
254
  var pendingMixAcks = /* @__PURE__ */ new Map();
249
255
  var pendingSttAcks = /* @__PURE__ */ new Map();
256
+ var pendingVoiceLanguageAcks = /* @__PURE__ */ new Map();
250
257
  function handleRecordingControlAck(message) {
251
258
  const pending = pendingRecordingAcks.get(message.requestId);
252
259
  if (!pending)
@@ -349,6 +356,24 @@ function handleSttControlAck(message) {
349
356
  requestId: message.requestId
350
357
  });
351
358
  }
359
+ function handleVoiceLanguageControlAck(message) {
360
+ const pending = pendingVoiceLanguageAcks.get(message.requestId);
361
+ if (!pending)
362
+ return;
363
+ clearTimeout(pending.timer);
364
+ pendingVoiceLanguageAcks.delete(message.requestId);
365
+ pending.resolve({
366
+ ok: message.ok,
367
+ reason: message.reason,
368
+ requestId: message.requestId,
369
+ ...message.language ? { language: message.language } : {},
370
+ ...message.voice ? { voice: message.voice } : {},
371
+ ...message.stt ? { stt: message.stt } : {},
372
+ ...message.scope ? { scope: message.scope } : {},
373
+ ...message.sttProvider ? { sttProvider: message.sttProvider } : {},
374
+ ...message.ttsProvider ? { ttsProvider: message.ttsProvider } : {}
375
+ });
376
+ }
352
377
  function clearPendingRecordingAcksForSession(sessionId, reason) {
353
378
  for (const [requestId, pending] of pendingRecordingAcks) {
354
379
  if (pending.sessionId !== sessionId)
@@ -414,7 +439,7 @@ function allowOutboundForSession(sessionId) {
414
439
  }
415
440
  function sendParentMessage(message) {
416
441
  const msgType = message && typeof message === "object" && "type" in message ? message.type : void 0;
417
- if (msgType === "mix_control" || msgType === "stt_control") {
442
+ if (msgType === "mix_control" || msgType === "stt_control" || msgType === "voice_language_control") {
418
443
  process.send?.(message);
419
444
  return;
420
445
  }
@@ -624,6 +649,10 @@ function defineAgent(handlers) {
624
649
  handleSttControlAck(message);
625
650
  return;
626
651
  }
652
+ if (isVoiceLanguageControlAckMessage(message)) {
653
+ handleVoiceLanguageControlAck(message);
654
+ return;
655
+ }
627
656
  if (isWebhookMessage(message)) {
628
657
  void agentStartReady.then(() => handleWebhookMessage(message, handlers));
629
658
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voicethere/agent",
3
- "version": "0.8.3",
3
+ "version": "0.9.0",
4
4
  "description": "VoiceThere customer agent SDK — IPC types and runtime helpers for sandboxed child bundles",
5
5
  "type": "module",
6
6
  "exports": {
@@ -19,7 +19,7 @@ import {
19
19
 
20
20
  | Kind | Dashboard create | Prebuilt seed bundle | Typical consumer |
21
21
  | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | ----------------------- |
22
- | **product** | Yes (`echo`, `echo-dc`, `voice-starter`, `world-sync`, `world-sync-binary`, `game-sync`, `voice-showcase`, `recording-consent`, `positional-tts`, `spatial-showcase`, `webhooks`, `webhooks-redis`) | Yes — `dist/templates/<id>/agent.js` | Platform project create |
22
+ | **product** | Yes (`echo`, `echo-dc`, `voice-starter`, `language-switch`, `world-sync`, `world-sync-binary`, `game-sync`, `voice-showcase`, `recording-consent`, `positional-tts`, `spatial-showcase`, `webhooks`, `webhooks-redis`) | Yes — `dist/templates/<id>/agent.js` | Platform project create |
23
23
  | **e2e** | No | No — build from sources at test time | `voicethere/e2e` smokes |
24
24
 
25
25
  Product templates always set `seedOnCreate: true`. CI fails if a product template is missing its prebuilt bundle after `npm run build`.
@@ -64,6 +64,7 @@ Each folder has its own README. Summary:
64
64
  | `echo` | `echo/` | Voice + chat echo |
65
65
  | `echo-dc` | `echo-dc/` | Data-channel echo, no TTS |
66
66
  | `voice-starter` | `voice-starter/` | Every speech event |
67
+ | `language-switch` | `language-switch/` | Separate `setVoiceLanguage` calls for STT, TTS, and vendor |
67
68
  | `world-sync` | `world-sync/` | JSON pose broadcast |
68
69
  | `world-sync-binary` | `world-sync-binary/` | Binary pose `ArrayBuffer` |
69
70
  | `game-sync` | `game-sync/` | Authoritative sim, Redis + binary snapshots |
@@ -0,0 +1,21 @@
1
+ # language-switch
2
+
3
+ Spoken-language detection tells the agent which ISO 639-1 code it heard. It does **not** change the TTS voice or the STT model. This template changes the two sides separately with `setVoiceLanguage`:
4
+
5
+ - `scope: "tts"` switches only the speaking voice
6
+ - `scope: "stt"` switches only the listening model
7
+
8
+ A Sherpa language with no STT id (`it`, `pt`, `nl`, `pl`, `hi`) fails the STT call and still switches TTS.
9
+
10
+ Chat commands change one vendor while the session stays connected. API keys are project secrets on the running deploy, not arguments:
11
+
12
+ - `/tts sherpa de` — Sherpa TTS only
13
+ - `/stt sherpa de` — Sherpa STT only
14
+ - `/tts elevenlabs` — ElevenLabs TTS, current STT stays
15
+ - `/stt deepgram de` — Deepgram STT, current voice stays
16
+
17
+ English finals are echoed as `you said: …` after a short wait, so a language event for the same utterance can cancel the echo. The revealing utterance is not spoken back as an English transcript.
18
+
19
+ `voice` and `stt` are Sherpa catalog ids (`de`, `en-lessac`, `en-small`), listed in the spoken-language docs. Vendor ids are `local-sherpa`, `openai`, `deepgram`, `assemblyai`, `google`, `elevenlabs`, and `cartesia`.
20
+
21
+ Entry: `templates/language-switch/agent.ts`.
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Change STT and TTS separately, including the vendor, while the call stays up.
3
+ *
4
+ * The runner reports `user_language` and does not change either side. This
5
+ * agent switches the Sherpa voice and the Sherpa STT model as two calls, so
6
+ * one side can fail without blocking the other. Chat commands change a single
7
+ * vendor mid-conversation:
8
+ *
9
+ * - `/tts sherpa de` — Sherpa TTS only
10
+ * - `/stt sherpa de` — Sherpa STT only
11
+ * - `/tts elevenlabs` — ElevenLabs TTS, current STT stays
12
+ * - `/stt deepgram de` — Deepgram STT, current voice stays
13
+ *
14
+ * Vendor API keys are project secrets already on the running deploy. Do not
15
+ * put keys in this file. See /docs/spoken-language.
16
+ */
17
+ import {
18
+ agentLog,
19
+ defineAgent,
20
+ parseChatText,
21
+ setVoiceLanguage,
22
+ speak,
23
+ type VoiceLanguageResult,
24
+ } from "@voicethere/agent";
25
+
26
+ const ECHO_PREFIX = "you said:";
27
+ const ECHO_WAIT_MS = 4000;
28
+
29
+ const REPLIES: Record<string, string> = {
30
+ de: "Guten Tag. Ich antworte jetzt auf Deutsch.",
31
+ es: "Hola. Ahora respondo en español.",
32
+ fr: "Bonjour. Je réponds maintenant en français.",
33
+ it: "Ciao. Adesso rispondo in italiano.",
34
+ pt: "Olá. Agora respondo em português.",
35
+ nl: "Hallo. Ik antwoord nu in het Nederlands.",
36
+ pl: "Dzień dobry. Teraz odpowiadam po polsku.",
37
+ ru: "Здравствуйте. Теперь я отвечаю по-русски.",
38
+ };
39
+
40
+ type SessionLanguage = {
41
+ language: string;
42
+ echoTimer: ReturnType<typeof setTimeout> | undefined;
43
+ suppressNextFinal: boolean;
44
+ };
45
+
46
+ const sessions = new Map<string, SessionLanguage>();
47
+
48
+ function stateFor(sessionId: string): SessionLanguage {
49
+ const existing = sessions.get(sessionId);
50
+ if (existing) return existing;
51
+ const created: SessionLanguage = {
52
+ language: "en",
53
+ echoTimer: undefined,
54
+ suppressNextFinal: false,
55
+ };
56
+ sessions.set(sessionId, created);
57
+ return created;
58
+ }
59
+
60
+ function replyFor(language: string): string {
61
+ return REPLIES[language] ?? `Continuing in ${language}.`;
62
+ }
63
+
64
+ function logSwitch(sessionId: string, side: string, result: VoiceLanguageResult): void {
65
+ if (!result.ok) {
66
+ agentLog("warn", `setVoiceLanguage ${side} failed: ${result.reason ?? "unknown"}`);
67
+ return;
68
+ }
69
+ const provider = side === "tts" ? result.ttsProvider : result.sttProvider;
70
+ const model = side === "tts" ? result.voice : result.stt;
71
+ agentLog(
72
+ "info",
73
+ `voice ${side} ${sessionId} ${result.language ?? ""} provider=${provider ?? "unchanged"} model=${model ?? "unchanged"}`,
74
+ );
75
+ }
76
+
77
+ defineAgent({
78
+ onSessionStart({ sessionId }) {
79
+ stateFor(sessionId);
80
+ agentLog("info", `language-switch session_start ${sessionId}`);
81
+ },
82
+
83
+ async onUserLanguage({ sessionId, language }) {
84
+ const state = stateFor(sessionId);
85
+ if (!language || language === state.language) return;
86
+
87
+ if (state.echoTimer) {
88
+ clearTimeout(state.echoTimer);
89
+ state.echoTimer = undefined;
90
+ } else {
91
+ state.suppressNextFinal = true;
92
+ }
93
+
94
+ const tts = await setVoiceLanguage(sessionId, {
95
+ scope: "tts",
96
+ language,
97
+ voice: language,
98
+ });
99
+ logSwitch(sessionId, "tts", tts);
100
+
101
+ const stt = await setVoiceLanguage(sessionId, {
102
+ scope: "stt",
103
+ language,
104
+ stt: language,
105
+ });
106
+ logSwitch(sessionId, "stt", stt);
107
+
108
+ if (!tts.ok) {
109
+ state.suppressNextFinal = false;
110
+ return;
111
+ }
112
+
113
+ state.language = language;
114
+ speak(sessionId, replyFor(language));
115
+ },
116
+
117
+ async onDataChannelMessage(ctx) {
118
+ const text = parseChatText(ctx.message);
119
+ if (!text) return;
120
+ const [command, vendor, language] = text.trim().split(/\s+/);
121
+ if (command === "/tts" && vendor === "sherpa" && language) {
122
+ const result = await setVoiceLanguage(ctx.sessionId, {
123
+ scope: "tts",
124
+ language,
125
+ ttsVendor: { provider: "local-sherpa", voice: language },
126
+ });
127
+ logSwitch(ctx.sessionId, "tts", result);
128
+ if (result.ok) speak(ctx.sessionId, replyFor(language));
129
+ return;
130
+ }
131
+ if (command === "/stt" && vendor === "sherpa" && language) {
132
+ const result = await setVoiceLanguage(ctx.sessionId, {
133
+ scope: "stt",
134
+ language,
135
+ sttVendor: { provider: "local-sherpa", model: language },
136
+ });
137
+ logSwitch(ctx.sessionId, "stt", result);
138
+ return;
139
+ }
140
+ if (command === "/tts" && vendor === "elevenlabs") {
141
+ const result = await setVoiceLanguage(ctx.sessionId, {
142
+ scope: "tts",
143
+ ttsVendor: { provider: "elevenlabs", model: "eleven_multilingual_v2" },
144
+ });
145
+ logSwitch(ctx.sessionId, "tts", result);
146
+ if (result.ok) speak(ctx.sessionId, "Speaking with ElevenLabs.");
147
+ return;
148
+ }
149
+ if (command === "/stt" && vendor === "deepgram") {
150
+ const result = await setVoiceLanguage(ctx.sessionId, {
151
+ scope: "stt",
152
+ sttVendor: {
153
+ provider: "deepgram",
154
+ model: "nova-3",
155
+ language: language || "en",
156
+ },
157
+ });
158
+ logSwitch(ctx.sessionId, "stt", result);
159
+ }
160
+ },
161
+
162
+ onUserSpeechFinal({ sessionId, text }) {
163
+ const state = stateFor(sessionId);
164
+ if (state.suppressNextFinal) {
165
+ state.suppressNextFinal = false;
166
+ return;
167
+ }
168
+ if (state.echoTimer) clearTimeout(state.echoTimer);
169
+ state.echoTimer = setTimeout(() => {
170
+ state.echoTimer = undefined;
171
+ speak(sessionId, `${ECHO_PREFIX} ${text}`.trim());
172
+ }, ECHO_WAIT_MS);
173
+ },
174
+
175
+ onSessionEnd({ sessionId }) {
176
+ const state = sessions.get(sessionId);
177
+ if (state?.echoTimer) clearTimeout(state.echoTimer);
178
+ sessions.delete(sessionId);
179
+ },
180
+ });
@@ -179,7 +179,9 @@ defineAgent({
179
179
  },
180
180
 
181
181
  onUserLanguage({ sessionId, language }) {
182
- // Route prompts / TTS voice from the detected ISO 639-1 code
182
+ // Detection does not change TTS or STT. Call setVoiceLanguage with
183
+ // scope "tts" or "stt" — or pass sttVendor / ttsVendor to change vendor.
184
+ // See templates/language-switch/agent.ts.
183
185
  agentLog("info", `user_language ${sessionId} ${language}`);
184
186
  },
185
187