@voicethere/agent 0.8.2 → 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.
- package/README.md +2 -1
- package/dist/agent.js +29 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/protocol.d.ts +71 -2
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js.map +1 -1
- package/dist/runtime.d.ts +43 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +109 -1
- package/dist/runtime.js.map +1 -1
- package/dist/templates/echo/agent.js +31 -2
- package/dist/templates/echo-dc/agent.js +31 -2
- package/dist/templates/game-sync/agent.js +31 -2
- package/dist/templates/language-switch/agent.js +1050 -0
- package/dist/templates/positional-tts/agent.js +31 -2
- package/dist/templates/recording-consent/agent.js +31 -2
- package/dist/templates/registry.d.ts.map +1 -1
- package/dist/templates/registry.js +8 -0
- package/dist/templates/registry.js.map +1 -1
- package/dist/templates/spatial-showcase/agent.js +37 -5
- package/dist/templates/voice-showcase/agent.js +31 -2
- package/dist/templates/voice-starter/agent.js +31 -2
- package/dist/templates/webhooks/agent.js +31 -2
- package/dist/templates/webhooks-redis/agent.js +31 -2
- package/dist/templates/world-sync/agent.js +31 -2
- package/dist/templates/world-sync-binary/agent.js +31 -2
- package/package.json +1 -1
- package/templates/README.md +2 -1
- package/templates/language-switch/README.md +21 -0
- package/templates/language-switch/agent.ts +180 -0
- package/templates/spatial-showcase/agent.ts +4 -3
- package/templates/spatial-showcase/sounds.ts +2 -0
- package/templates/voice-starter/agent.ts +3 -1
|
@@ -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;
|
|
@@ -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
package/templates/README.md
CHANGED
|
@@ -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
|
+
});
|
|
@@ -56,8 +56,9 @@ import {
|
|
|
56
56
|
orbitSineClipDurationMs,
|
|
57
57
|
} from "./sine.js";
|
|
58
58
|
|
|
59
|
-
const
|
|
60
|
-
const
|
|
59
|
+
const ORBIT_POSE_HZ = 60;
|
|
60
|
+
const ORBIT_TICK_MS = 1000 / ORBIT_POSE_HZ;
|
|
61
|
+
const ORBIT_POSE_EMIT_MS = ORBIT_TICK_MS;
|
|
61
62
|
const ROOM_STATE_TICK_MS = 100;
|
|
62
63
|
const LOOP_PAD_POLL_MS = 250;
|
|
63
64
|
const ORBIT_SINE_POLL_MS = 250;
|
|
@@ -426,7 +427,7 @@ function emitOrbitPose(sessionId: string, state: SessionState): void {
|
|
|
426
427
|
const now = Date.now();
|
|
427
428
|
if (
|
|
428
429
|
state.lastOrbitPoseEmitMs !== undefined &&
|
|
429
|
-
now - state.lastOrbitPoseEmitMs < ORBIT_POSE_EMIT_MS
|
|
430
|
+
now - state.lastOrbitPoseEmitMs < ORBIT_POSE_EMIT_MS - 2
|
|
430
431
|
) {
|
|
431
432
|
return;
|
|
432
433
|
}
|
|
@@ -35,6 +35,8 @@ export const SHOWCASE_SOUND_FILES: Record<ShowcaseClipId, ShowcaseSoundFile> = {
|
|
|
35
35
|
export const SHOWCASE_ASSET_ORIGINS = [
|
|
36
36
|
"https://app.voicethere.io",
|
|
37
37
|
"https://app.voicethere.dev",
|
|
38
|
+
"https://www.voicethere.io",
|
|
39
|
+
"https://voicethere.io",
|
|
38
40
|
"http://localhost:3000",
|
|
39
41
|
] as const;
|
|
40
42
|
|
|
@@ -179,7 +179,9 @@ defineAgent({
|
|
|
179
179
|
},
|
|
180
180
|
|
|
181
181
|
onUserLanguage({ sessionId, language }) {
|
|
182
|
-
//
|
|
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
|
|