@voicethere/agent 0.9.1 → 0.9.3
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/dist/templates/language-switch/agent.js +133 -22
- package/dist/templates/registry.d.ts.map +1 -1
- package/dist/templates/registry.js +5 -2
- package/dist/templates/registry.js.map +1 -1
- package/package.json +1 -1
- package/templates/README.md +1 -1
- package/templates/language-switch/README.md +13 -2
- package/templates/language-switch/agent.ts +203 -25
|
@@ -941,6 +941,61 @@ function parseChatText(message) {
|
|
|
941
941
|
// templates/language-switch/agent.ts
|
|
942
942
|
var ECHO_PREFIX = "you said:";
|
|
943
943
|
var ECHO_WAIT_MS = 4e3;
|
|
944
|
+
var ECHO_PREFIXES = {
|
|
945
|
+
en: "you said:",
|
|
946
|
+
de: "Du hast gesagt:",
|
|
947
|
+
es: "Dijiste:",
|
|
948
|
+
fr: "Tu as dit :",
|
|
949
|
+
it: "Hai detto:",
|
|
950
|
+
pt: "Voc\xEA disse:",
|
|
951
|
+
nl: "Je zei:",
|
|
952
|
+
pl: "Powiedzia\u0142e\u015B:",
|
|
953
|
+
ru: "\u0412\u044B \u0441\u043A\u0430\u0437\u0430\u043B\u0438:"
|
|
954
|
+
};
|
|
955
|
+
var WAIT_MESSAGES = {
|
|
956
|
+
en: "One moment please, I'm switching to your language.",
|
|
957
|
+
de: "Einen Moment bitte, ich wechsle zu Ihrer Sprache.",
|
|
958
|
+
es: "Un momento, por favor, estoy cambiando a su idioma.",
|
|
959
|
+
fr: "Un instant, je passe dans votre langue.",
|
|
960
|
+
it: "Un momento, per favore, sto passando alla tua lingua.",
|
|
961
|
+
pt: "Um momento, por favor, estou mudando para o seu idioma.",
|
|
962
|
+
nl: "Een moment alstublieft, ik schakel over naar uw taal.",
|
|
963
|
+
pl: "Chwileczk\u0119, prze\u0142\u0105czam si\u0119 na Tw\xF3j j\u0119zyk.",
|
|
964
|
+
ru: "\u041E\u0434\u043D\u0443 \u043C\u0438\u043D\u0443\u0442\u0443, \u044F \u043F\u0435\u0440\u0435\u0445\u043E\u0436\u0443 \u043D\u0430 \u0432\u0430\u0448 \u044F\u0437\u044B\u043A."
|
|
965
|
+
};
|
|
966
|
+
var FAILED_MESSAGES = {
|
|
967
|
+
en: "Sorry, I couldn't switch languages. I'll keep going in English.",
|
|
968
|
+
de: "Entschuldigung, der Sprachwechsel hat nicht geklappt. Ich bleibe bei Deutsch.",
|
|
969
|
+
es: "Lo siento, no pude cambiar de idioma. Sigo en espa\xF1ol.",
|
|
970
|
+
fr: "D\xE9sol\xE9, je n'ai pas pu changer de langue. Je continue en fran\xE7ais.",
|
|
971
|
+
it: "Mi dispiace, non sono riuscito a cambiare lingua. Continuo in italiano.",
|
|
972
|
+
pt: "Desculpe, n\xE3o consegui mudar de idioma. Continuo em portugu\xEAs.",
|
|
973
|
+
nl: "Sorry, het wisselen van taal is niet gelukt. Ik blijf Nederlands spreken.",
|
|
974
|
+
pl: "Przepraszam, nie uda\u0142o si\u0119 zmieni\u0107 j\u0119zyka. Zostaj\u0119 przy polskim.",
|
|
975
|
+
ru: "\u0418\u0437\u0432\u0438\u043D\u0438\u0442\u0435, \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u043C\u0435\u043D\u0438\u0442\u044C \u044F\u0437\u044B\u043A. \u041F\u0440\u043E\u0434\u043E\u043B\u0436\u0430\u044E \u043F\u043E-\u0440\u0443\u0441\u0441\u043A\u0438."
|
|
976
|
+
};
|
|
977
|
+
var WAIT_MESSAGES_ENV = "LANGUAGE_SWITCH_WAIT_MESSAGES_JSON";
|
|
978
|
+
var FAILED_MESSAGES_ENV = "LANGUAGE_SWITCH_FAILED_MESSAGES_JSON";
|
|
979
|
+
function parseMessageOverrides(raw) {
|
|
980
|
+
if (!raw) return {};
|
|
981
|
+
let parsed;
|
|
982
|
+
try {
|
|
983
|
+
parsed = JSON.parse(raw);
|
|
984
|
+
} catch {
|
|
985
|
+
return {};
|
|
986
|
+
}
|
|
987
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
988
|
+
const out = {};
|
|
989
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
990
|
+
if (typeof value === "string" && value.trim()) {
|
|
991
|
+
out[key.trim().toLowerCase()] = value.trim();
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
return out;
|
|
995
|
+
}
|
|
996
|
+
function pickMessage(defaults, overrides, language) {
|
|
997
|
+
return overrides[language] ?? defaults[language] ?? overrides.en ?? defaults.en;
|
|
998
|
+
}
|
|
944
999
|
var REPLIES = {
|
|
945
1000
|
de: "Guten Tag. Ich antworte jetzt auf Deutsch.",
|
|
946
1001
|
es: "Hola. Ahora respondo en espa\xF1ol.",
|
|
@@ -959,7 +1014,11 @@ function stateFor(sessionId, env) {
|
|
|
959
1014
|
language: "en",
|
|
960
1015
|
runnerAutoSwitch: env ? isRunnerLidAutoSwitchEnabled(env) : false,
|
|
961
1016
|
echoTimer: void 0,
|
|
962
|
-
suppressNextFinal: false
|
|
1017
|
+
suppressNextFinal: false,
|
|
1018
|
+
pendingSwitch: void 0,
|
|
1019
|
+
switching: false,
|
|
1020
|
+
waitOverrides: parseMessageOverrides(env?.[WAIT_MESSAGES_ENV]),
|
|
1021
|
+
failedOverrides: parseMessageOverrides(env?.[FAILED_MESSAGES_ENV])
|
|
963
1022
|
};
|
|
964
1023
|
sessions.set(sessionId, created);
|
|
965
1024
|
return created;
|
|
@@ -967,6 +1026,9 @@ function stateFor(sessionId, env) {
|
|
|
967
1026
|
function replyFor(language) {
|
|
968
1027
|
return REPLIES[language] ?? `Continuing in ${language}.`;
|
|
969
1028
|
}
|
|
1029
|
+
function echoPrefixFor(language) {
|
|
1030
|
+
return ECHO_PREFIXES[language] ?? ECHO_PREFIXES.en;
|
|
1031
|
+
}
|
|
970
1032
|
function logSwitch(sessionId, side, result) {
|
|
971
1033
|
if (!result.ok) {
|
|
972
1034
|
agentLog(
|
|
@@ -982,6 +1044,44 @@ function logSwitch(sessionId, side, result) {
|
|
|
982
1044
|
`voice ${side} ${sessionId} ${result.language ?? ""} provider=${provider ?? "unchanged"} model=${model ?? "unchanged"}`
|
|
983
1045
|
);
|
|
984
1046
|
}
|
|
1047
|
+
var WAIT_SPEECH_TIMEOUT_MS = 8e3;
|
|
1048
|
+
function startSwitch(sessionId, state) {
|
|
1049
|
+
const pending = state.pendingSwitch;
|
|
1050
|
+
if (!pending) return;
|
|
1051
|
+
clearTimeout(pending.timer);
|
|
1052
|
+
state.pendingSwitch = void 0;
|
|
1053
|
+
state.switching = true;
|
|
1054
|
+
void runSwitch(sessionId, state, pending.language, pending.previous).catch((error) => {
|
|
1055
|
+
state.suppressNextFinal = false;
|
|
1056
|
+
agentLog("warn", `language-switch ${sessionId} failed: ${String(error)}`);
|
|
1057
|
+
}).finally(() => {
|
|
1058
|
+
state.switching = false;
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
async function runSwitch(sessionId, state, language, previous) {
|
|
1062
|
+
const tts = await setVoiceLanguage(sessionId, {
|
|
1063
|
+
scope: "tts",
|
|
1064
|
+
language,
|
|
1065
|
+
voice: language
|
|
1066
|
+
});
|
|
1067
|
+
logSwitch(sessionId, "tts", tts);
|
|
1068
|
+
const stt = await setVoiceLanguage(sessionId, {
|
|
1069
|
+
scope: "stt",
|
|
1070
|
+
language,
|
|
1071
|
+
stt: language
|
|
1072
|
+
});
|
|
1073
|
+
logSwitch(sessionId, "stt", stt);
|
|
1074
|
+
if (!tts.ok) {
|
|
1075
|
+
state.suppressNextFinal = false;
|
|
1076
|
+
speak(
|
|
1077
|
+
sessionId,
|
|
1078
|
+
pickMessage(FAILED_MESSAGES, state.failedOverrides, previous)
|
|
1079
|
+
);
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
state.language = language;
|
|
1083
|
+
speak(sessionId, replyFor(language));
|
|
1084
|
+
}
|
|
985
1085
|
function prepareLanguageTransition(state) {
|
|
986
1086
|
if (state.echoTimer) {
|
|
987
1087
|
clearTimeout(state.echoTimer);
|
|
@@ -1008,34 +1108,31 @@ defineAgent({
|
|
|
1008
1108
|
async onUserLanguage({ sessionId, language }) {
|
|
1009
1109
|
const state = stateFor(sessionId);
|
|
1010
1110
|
if (!language || language === state.language) return;
|
|
1011
|
-
prepareLanguageTransition(state);
|
|
1012
1111
|
if (state.runnerAutoSwitch) {
|
|
1013
1112
|
agentLog(
|
|
1014
1113
|
"info",
|
|
1015
|
-
`LID detected ${language}; runner auto-switch applies STT/TTS \u2014 agent
|
|
1114
|
+
`LID detected ${language}; runner auto-switch applies STT/TTS \u2014 agent only remembers the language`
|
|
1016
1115
|
);
|
|
1017
1116
|
state.language = language;
|
|
1018
|
-
speak(sessionId, replyFor(language));
|
|
1019
1117
|
return;
|
|
1020
1118
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
});
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
state
|
|
1038
|
-
speak(sessionId, replyFor(language));
|
|
1119
|
+
if (state.pendingSwitch || state.switching) return;
|
|
1120
|
+
prepareLanguageTransition(state);
|
|
1121
|
+
const previous = state.language;
|
|
1122
|
+
speak(sessionId, pickMessage(WAIT_MESSAGES, state.waitOverrides, previous));
|
|
1123
|
+
const timer = setTimeout(() => {
|
|
1124
|
+
agentLog(
|
|
1125
|
+
"warn",
|
|
1126
|
+
`language-switch ${sessionId} no agent_speaking_end within ${WAIT_SPEECH_TIMEOUT_MS}ms; switching anyway`
|
|
1127
|
+
);
|
|
1128
|
+
startSwitch(sessionId, state);
|
|
1129
|
+
}, WAIT_SPEECH_TIMEOUT_MS);
|
|
1130
|
+
state.pendingSwitch = { language, previous, timer };
|
|
1131
|
+
},
|
|
1132
|
+
onSpeechEvent({ sessionId }, event) {
|
|
1133
|
+
if (event.type !== "agent_speaking_end") return;
|
|
1134
|
+
const state = sessions.get(sessionId);
|
|
1135
|
+
if (state?.pendingSwitch) startSwitch(sessionId, state);
|
|
1039
1136
|
},
|
|
1040
1137
|
async onVoiceLanguageChanged({ sessionId, language }) {
|
|
1041
1138
|
const state = stateFor(sessionId);
|
|
@@ -1094,6 +1191,10 @@ defineAgent({
|
|
|
1094
1191
|
},
|
|
1095
1192
|
onUserSpeechFinal({ sessionId, text }) {
|
|
1096
1193
|
const state = stateFor(sessionId);
|
|
1194
|
+
if (state.runnerAutoSwitch) {
|
|
1195
|
+
speak(sessionId, `${echoPrefixFor(state.language)} ${text}`.trim());
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1097
1198
|
if (state.suppressNextFinal) {
|
|
1098
1199
|
state.suppressNextFinal = false;
|
|
1099
1200
|
return;
|
|
@@ -1107,6 +1208,16 @@ defineAgent({
|
|
|
1107
1208
|
onSessionEnd({ sessionId }) {
|
|
1108
1209
|
const state = sessions.get(sessionId);
|
|
1109
1210
|
if (state?.echoTimer) clearTimeout(state.echoTimer);
|
|
1211
|
+
if (state?.pendingSwitch) clearTimeout(state.pendingSwitch.timer);
|
|
1110
1212
|
sessions.delete(sessionId);
|
|
1111
1213
|
}
|
|
1112
1214
|
});
|
|
1215
|
+
export {
|
|
1216
|
+
FAILED_MESSAGES,
|
|
1217
|
+
FAILED_MESSAGES_ENV,
|
|
1218
|
+
WAIT_MESSAGES,
|
|
1219
|
+
WAIT_MESSAGES_ENV,
|
|
1220
|
+
WAIT_SPEECH_TIMEOUT_MS,
|
|
1221
|
+
parseMessageOverrides,
|
|
1222
|
+
pickMessage
|
|
1223
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/templates/registry.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,KAAK,CAAC;AAE7C,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,mFAAmF;IACnF,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,YAAY,CAAC;IACnB,mFAAmF;IACnF,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,yEAAyE;IACzE,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,oFAAoF;IACpF,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC1C;AAED,eAAO,MAAM,eAAe,EAAE,SAAS,uBAAuB,
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/templates/registry.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,KAAK,CAAC;AAE7C,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,MAAM,CAAC;IACX,mFAAmF;IACnF,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,YAAY,CAAC;IACnB,mFAAmF;IACnF,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,yEAAyE;IACzE,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,oFAAoF;IACpF,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC1C;AAED,eAAO,MAAM,eAAe,EAAE,SAAS,uBAAuB,EAiMpD,CAAC;AAEX,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC;AAMrE,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,GAAG,EAAE,IAAI,eAAe,CAEnE;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,uBAAuB,CAMnE;AAED,wBAAgB,yBAAyB,IAAI,uBAAuB,EAAE,CAErE"}
|
|
@@ -20,7 +20,7 @@ export const AGENT_TEMPLATES = [
|
|
|
20
20
|
entry: "language-switch/agent.ts",
|
|
21
21
|
kind: "product",
|
|
22
22
|
seedOnCreate: true,
|
|
23
|
-
description: "Switches STT and TTS independently from onUserLanguage, and can change one vendor mid-conversation.
|
|
23
|
+
description: "Switches STT and TTS independently from onUserLanguage, and can change one vendor mid-conversation. With project auto-switch on, the runner switches and replays the utterance, and the agent answers the recognized text in the new language. With auto-switch off, detection does not change either side until this agent calls setVoiceLanguage.",
|
|
24
24
|
sourceFiles: ["language-switch/agent.ts"],
|
|
25
25
|
},
|
|
26
26
|
{
|
|
@@ -45,7 +45,10 @@ export const AGENT_TEMPLATES = [
|
|
|
45
45
|
kind: "product",
|
|
46
46
|
seedOnCreate: true,
|
|
47
47
|
description: "Single-agent binary world sync — onDataChannelBinary + sendBinaryToClient ArrayBuffer poses, no Redis.",
|
|
48
|
-
sourceFiles: [
|
|
48
|
+
sourceFiles: [
|
|
49
|
+
"world-sync-binary/agent.ts",
|
|
50
|
+
"world-sync-binary/protocol.ts",
|
|
51
|
+
],
|
|
49
52
|
},
|
|
50
53
|
{
|
|
51
54
|
id: "game-sync",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/templates/registry.ts"],"names":[],"mappings":"AAgBA,MAAM,CAAC,MAAM,eAAe,GAAuC;IACjE;QACE,EAAE,EAAE,MAAM;QACV,KAAK,EAAE,eAAe;QACtB,IAAI,EAAE,SAAS;QACf,YAAY,EAAE,IAAI;QAClB,WAAW,EACT,8EAA8E;QAChF,WAAW,EAAE,CAAC,eAAe,CAAC;KAC/B;IACD;QACE,EAAE,EAAE,SAAS;QACb,KAAK,EAAE,kBAAkB;QACzB,IAAI,EAAE,SAAS;QACf,YAAY,EAAE,IAAI;QAClB,WAAW,EACT,qEAAqE;QACvE,WAAW,EAAE,CAAC,kBAAkB,CAAC;KAClC;IACD;QACE,EAAE,EAAE,iBAAiB;QACrB,KAAK,EAAE,0BAA0B;QACjC,IAAI,EAAE,SAAS;QACf,YAAY,EAAE,IAAI;QAClB,WAAW,EACT,
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/templates/registry.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/templates/README.md
CHANGED
|
@@ -64,7 +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/` | Manual `setVoiceLanguage
|
|
67
|
+
| `language-switch` | `language-switch/` | Manual `setVoiceLanguage`, or runner auto-switch with a localized echo of the recognized text |
|
|
68
68
|
| `world-sync` | `world-sync/` | JSON pose broadcast |
|
|
69
69
|
| `world-sync-binary` | `world-sync-binary/` | Binary pose `ArrayBuffer` |
|
|
70
70
|
| `game-sync` | `game-sync/` | Authoritative sim, Redis + binary snapshots |
|
|
@@ -9,11 +9,22 @@ Leave the project **spoken-language auto-switch** setting off. When `onUserLangu
|
|
|
9
9
|
- `scope: "tts"` switches only the speaking voice
|
|
10
10
|
- `scope: "stt"` switches only the listening model
|
|
11
11
|
|
|
12
|
+
The runner plays nothing for a manual switch, and the target voice and STT pools can take several seconds to warm up. So the agent first speaks a short wait message in the language it is leaving, using the current voice (for example `One moment please, I'm switching to your language.` in English, `Einen Moment bitte, ich wechsle zu Ihrer Sprache.` in German). The runner swaps the voice without draining it, so the agent waits for the `agent_speaking_end` speech event (at most 8 seconds, `WAIT_SPEECH_TIMEOUT_MS`) before it calls `setVoiceLanguage` for TTS and STT and replies in the new language. A second language event during the wait or the switch is ignored. The utterance that revealed the language is not echoed back. If the TTS switch fails (a timeout, for instance), the agent speaks a short fallback in the old language and stays in it.
|
|
13
|
+
|
|
14
|
+
The texts live in the `WAIT_MESSAGES` and `FAILED_MESSAGES` maps in `agent.ts` (en, de, es, fr, it, pt, nl, pl, ru). Override any of them per language with a JSON object in the session env, keyed by ISO 639-1 code:
|
|
15
|
+
|
|
16
|
+
- `LANGUAGE_SWITCH_WAIT_MESSAGES_JSON` — wait message, for example `{"en":"Hold on, switching languages."}`
|
|
17
|
+
- `LANGUAGE_SWITCH_FAILED_MESSAGES_JSON` — fallback after a failed switch
|
|
18
|
+
|
|
19
|
+
Invalid JSON or non-string values are ignored and the built-in text is used. A language without a text falls back to English.
|
|
20
|
+
|
|
12
21
|
A Sherpa language with no STT id (`it`, `pt`, `nl`, `pl`, `hi`) fails the STT call and still switches TTS.
|
|
13
22
|
|
|
14
23
|
## Mode B — Runner auto-switch
|
|
15
24
|
|
|
16
|
-
Enable auto-switch in the project voice settings (runner applies STT/TTS when LID detects a new language). When `session_start.env` includes a truthy `SHERPA_LID_AUTO_SWITCH`, this template **does not** call `setVoiceLanguage
|
|
25
|
+
Enable auto-switch in the project voice settings (runner applies STT/TTS when LID detects a new language). When `session_start.env` includes a truthy `SHERPA_LID_AUTO_SWITCH`, this template **does not** call `setVoiceLanguage`. The runner replays the utterance into the new language's STT, so the next final is the recognized text in the new language. The agent remembers the language from `onUserLanguage` / `onVoiceLanguageChanged` and answers each final right away with a short localized prefix, for example `you said: …` in English and `Du hast gesagt: …` in German. Use `onVoiceLanguageChanged` when you need the committed language after the runner applies the change.
|
|
26
|
+
|
|
27
|
+
In this mode the runner owns the wait message and the agent speaks no wait or fallback text of its own.
|
|
17
28
|
|
|
18
29
|
Detection and chat commands are unchanged: `/tts` and `/stt` still call `setVoiceLanguage` for one vendor at a time.
|
|
19
30
|
|
|
@@ -24,7 +35,7 @@ Chat commands change one vendor while the session stays connected. API keys are
|
|
|
24
35
|
- `/tts elevenlabs` — ElevenLabs TTS, current STT stays
|
|
25
36
|
- `/stt deepgram de` — Deepgram STT, current voice stays
|
|
26
37
|
|
|
27
|
-
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.
|
|
38
|
+
In manual mode, 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.
|
|
28
39
|
|
|
29
40
|
`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`.
|
|
30
41
|
|
|
@@ -4,9 +4,22 @@
|
|
|
4
4
|
* Two deployment modes (see README):
|
|
5
5
|
*
|
|
6
6
|
* (A) Project auto-switch off — this agent calls `setVoiceLanguage` from
|
|
7
|
-
* `onUserLanguage` when LID detects a new language.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* `onUserLanguage` when LID detects a new language. The runner plays
|
|
8
|
+
* nothing for manual switches and the target pools may be cold for
|
|
9
|
+
* several seconds, so the agent first speaks a wait message in the
|
|
10
|
+
* language being left (current voice), waits for `agent_speaking_end`
|
|
11
|
+
* (at most WAIT_SPEECH_TIMEOUT_MS) so the runner does not swap the voice
|
|
12
|
+
* mid-sentence, then switches TTS and STT, then replies in the new
|
|
13
|
+
* language. If the switch fails it speaks a short
|
|
14
|
+
* fallback in the old language and stays. Texts live in WAIT_MESSAGES /
|
|
15
|
+
* FAILED_MESSAGES and can be overridden per language with the session env
|
|
16
|
+
* vars LANGUAGE_SWITCH_WAIT_MESSAGES_JSON and
|
|
17
|
+
* LANGUAGE_SWITCH_FAILED_MESSAGES_JSON (JSON object, ISO 639-1 -> text).
|
|
18
|
+
* (B) Project enables runner auto-switch — STT/TTS are runner-owned. The
|
|
19
|
+
* runner replays the utterance into the new language's STT, so the next
|
|
20
|
+
* final is the correctly recognized text. The agent never calls
|
|
21
|
+
* `setVoiceLanguage`; it remembers the language and answers each final
|
|
22
|
+
* right away with a short localized prefix ("Du hast gesagt: …").
|
|
10
23
|
*
|
|
11
24
|
* Chat commands change a single vendor mid-conversation:
|
|
12
25
|
*
|
|
@@ -31,6 +44,83 @@ import {
|
|
|
31
44
|
const ECHO_PREFIX = "you said:";
|
|
32
45
|
const ECHO_WAIT_MS = 4000;
|
|
33
46
|
|
|
47
|
+
/** Echo prefix per language for runner auto-switch mode. */
|
|
48
|
+
const ECHO_PREFIXES: Record<string, string> = {
|
|
49
|
+
en: "you said:",
|
|
50
|
+
de: "Du hast gesagt:",
|
|
51
|
+
es: "Dijiste:",
|
|
52
|
+
fr: "Tu as dit :",
|
|
53
|
+
it: "Hai detto:",
|
|
54
|
+
pt: "Você disse:",
|
|
55
|
+
nl: "Je zei:",
|
|
56
|
+
pl: "Powiedziałeś:",
|
|
57
|
+
ru: "Вы сказали:",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** Spoken in the language being left, before a manual switch starts. */
|
|
61
|
+
export const WAIT_MESSAGES: Record<string, string> = {
|
|
62
|
+
en: "One moment please, I'm switching to your language.",
|
|
63
|
+
de: "Einen Moment bitte, ich wechsle zu Ihrer Sprache.",
|
|
64
|
+
es: "Un momento, por favor, estoy cambiando a su idioma.",
|
|
65
|
+
fr: "Un instant, je passe dans votre langue.",
|
|
66
|
+
it: "Un momento, per favore, sto passando alla tua lingua.",
|
|
67
|
+
pt: "Um momento, por favor, estou mudando para o seu idioma.",
|
|
68
|
+
nl: "Een moment alstublieft, ik schakel over naar uw taal.",
|
|
69
|
+
pl: "Chwileczkę, przełączam się na Twój język.",
|
|
70
|
+
ru: "Одну минуту, я перехожу на ваш язык.",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** Spoken in the old language when the switch fails and the agent stays. */
|
|
74
|
+
export const FAILED_MESSAGES: Record<string, string> = {
|
|
75
|
+
en: "Sorry, I couldn't switch languages. I'll keep going in English.",
|
|
76
|
+
de: "Entschuldigung, der Sprachwechsel hat nicht geklappt. Ich bleibe bei Deutsch.",
|
|
77
|
+
es: "Lo siento, no pude cambiar de idioma. Sigo en español.",
|
|
78
|
+
fr: "Désolé, je n'ai pas pu changer de langue. Je continue en français.",
|
|
79
|
+
it: "Mi dispiace, non sono riuscito a cambiare lingua. Continuo in italiano.",
|
|
80
|
+
pt: "Desculpe, não consegui mudar de idioma. Continuo em português.",
|
|
81
|
+
nl: "Sorry, het wisselen van taal is niet gelukt. Ik blijf Nederlands spreken.",
|
|
82
|
+
pl: "Przepraszam, nie udało się zmienić języka. Zostaję przy polskim.",
|
|
83
|
+
ru: "Извините, не удалось сменить язык. Продолжаю по-русски.",
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const WAIT_MESSAGES_ENV = "LANGUAGE_SWITCH_WAIT_MESSAGES_JSON";
|
|
87
|
+
export const FAILED_MESSAGES_ENV = "LANGUAGE_SWITCH_FAILED_MESSAGES_JSON";
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Parse a per-language override from a session env var. Accepts a JSON object
|
|
91
|
+
* of language code -> non-empty string; anything else yields no overrides.
|
|
92
|
+
*/
|
|
93
|
+
export function parseMessageOverrides(
|
|
94
|
+
raw: string | undefined,
|
|
95
|
+
): Record<string, string> {
|
|
96
|
+
if (!raw) return {};
|
|
97
|
+
let parsed: unknown;
|
|
98
|
+
try {
|
|
99
|
+
parsed = JSON.parse(raw);
|
|
100
|
+
} catch {
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
104
|
+
const out: Record<string, string> = {};
|
|
105
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
106
|
+
if (typeof value === "string" && value.trim()) {
|
|
107
|
+
out[key.trim().toLowerCase()] = value.trim();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Message for `language`: override, then default, then English default. */
|
|
114
|
+
export function pickMessage(
|
|
115
|
+
defaults: Record<string, string>,
|
|
116
|
+
overrides: Record<string, string>,
|
|
117
|
+
language: string,
|
|
118
|
+
): string {
|
|
119
|
+
return (
|
|
120
|
+
overrides[language] ?? defaults[language] ?? overrides.en ?? defaults.en!
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
34
124
|
const REPLIES: Record<string, string> = {
|
|
35
125
|
de: "Guten Tag. Ich antworte jetzt auf Deutsch.",
|
|
36
126
|
es: "Hola. Ahora respondo en español.",
|
|
@@ -47,6 +137,16 @@ type SessionLanguage = {
|
|
|
47
137
|
runnerAutoSwitch: boolean;
|
|
48
138
|
echoTimer: ReturnType<typeof setTimeout> | undefined;
|
|
49
139
|
suppressNextFinal: boolean;
|
|
140
|
+
pendingSwitch:
|
|
141
|
+
| {
|
|
142
|
+
language: string;
|
|
143
|
+
previous: string;
|
|
144
|
+
timer: ReturnType<typeof setTimeout>;
|
|
145
|
+
}
|
|
146
|
+
| undefined;
|
|
147
|
+
switching: boolean;
|
|
148
|
+
waitOverrides: Record<string, string>;
|
|
149
|
+
failedOverrides: Record<string, string>;
|
|
50
150
|
};
|
|
51
151
|
|
|
52
152
|
const sessions = new Map<string, SessionLanguage>();
|
|
@@ -62,6 +162,10 @@ function stateFor(
|
|
|
62
162
|
runnerAutoSwitch: env ? isRunnerLidAutoSwitchEnabled(env) : false,
|
|
63
163
|
echoTimer: undefined,
|
|
64
164
|
suppressNextFinal: false,
|
|
165
|
+
pendingSwitch: undefined,
|
|
166
|
+
switching: false,
|
|
167
|
+
waitOverrides: parseMessageOverrides(env?.[WAIT_MESSAGES_ENV]),
|
|
168
|
+
failedOverrides: parseMessageOverrides(env?.[FAILED_MESSAGES_ENV]),
|
|
65
169
|
};
|
|
66
170
|
sessions.set(sessionId, created);
|
|
67
171
|
return created;
|
|
@@ -71,6 +175,10 @@ function replyFor(language: string): string {
|
|
|
71
175
|
return REPLIES[language] ?? `Continuing in ${language}.`;
|
|
72
176
|
}
|
|
73
177
|
|
|
178
|
+
function echoPrefixFor(language: string): string {
|
|
179
|
+
return ECHO_PREFIXES[language] ?? ECHO_PREFIXES.en!;
|
|
180
|
+
}
|
|
181
|
+
|
|
74
182
|
function logSwitch(
|
|
75
183
|
sessionId: string,
|
|
76
184
|
side: string,
|
|
@@ -91,6 +199,61 @@ function logSwitch(
|
|
|
91
199
|
);
|
|
92
200
|
}
|
|
93
201
|
|
|
202
|
+
/** Upper bound for waiting on the wait message before switching anyway. */
|
|
203
|
+
export const WAIT_SPEECH_TIMEOUT_MS = 8000;
|
|
204
|
+
|
|
205
|
+
/** Consume the pending switch (once) and run it without blocking handlers. */
|
|
206
|
+
function startSwitch(sessionId: string, state: SessionLanguage): void {
|
|
207
|
+
const pending = state.pendingSwitch;
|
|
208
|
+
if (!pending) return;
|
|
209
|
+
clearTimeout(pending.timer);
|
|
210
|
+
state.pendingSwitch = undefined;
|
|
211
|
+
state.switching = true;
|
|
212
|
+
void runSwitch(sessionId, state, pending.language, pending.previous)
|
|
213
|
+
.catch((error: unknown) => {
|
|
214
|
+
state.suppressNextFinal = false;
|
|
215
|
+
agentLog("warn", `language-switch ${sessionId} failed: ${String(error)}`);
|
|
216
|
+
})
|
|
217
|
+
.finally(() => {
|
|
218
|
+
state.switching = false;
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function runSwitch(
|
|
223
|
+
sessionId: string,
|
|
224
|
+
state: SessionLanguage,
|
|
225
|
+
language: string,
|
|
226
|
+
previous: string,
|
|
227
|
+
): Promise<void> {
|
|
228
|
+
const tts = await setVoiceLanguage(sessionId, {
|
|
229
|
+
scope: "tts",
|
|
230
|
+
language,
|
|
231
|
+
voice: language,
|
|
232
|
+
});
|
|
233
|
+
logSwitch(sessionId, "tts", tts);
|
|
234
|
+
|
|
235
|
+
const stt = await setVoiceLanguage(sessionId, {
|
|
236
|
+
scope: "stt",
|
|
237
|
+
language,
|
|
238
|
+
stt: language,
|
|
239
|
+
});
|
|
240
|
+
logSwitch(sessionId, "stt", stt);
|
|
241
|
+
|
|
242
|
+
if (!tts.ok) {
|
|
243
|
+
state.suppressNextFinal = false;
|
|
244
|
+
// Switch failed (for example a timeout): the voice is unchanged, so
|
|
245
|
+
// apologise in the old language and stay.
|
|
246
|
+
speak(
|
|
247
|
+
sessionId,
|
|
248
|
+
pickMessage(FAILED_MESSAGES, state.failedOverrides, previous),
|
|
249
|
+
);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
state.language = language;
|
|
254
|
+
speak(sessionId, replyFor(language));
|
|
255
|
+
}
|
|
256
|
+
|
|
94
257
|
function prepareLanguageTransition(state: SessionLanguage): void {
|
|
95
258
|
if (state.echoTimer) {
|
|
96
259
|
clearTimeout(state.echoTimer);
|
|
@@ -120,39 +283,47 @@ defineAgent({
|
|
|
120
283
|
const state = stateFor(sessionId);
|
|
121
284
|
if (!language || language === state.language) return;
|
|
122
285
|
|
|
123
|
-
prepareLanguageTransition(state);
|
|
124
|
-
|
|
125
286
|
if (state.runnerAutoSwitch) {
|
|
287
|
+
// The runner switches STT/TTS and replays the utterance into the new
|
|
288
|
+
// STT; the next final is the real utterance, so nothing is suppressed
|
|
289
|
+
// and no fixed sentence is spoken.
|
|
126
290
|
agentLog(
|
|
127
291
|
"info",
|
|
128
|
-
`LID detected ${language}; runner auto-switch applies STT/TTS — agent
|
|
292
|
+
`LID detected ${language}; runner auto-switch applies STT/TTS — agent only remembers the language`,
|
|
129
293
|
);
|
|
130
294
|
state.language = language;
|
|
131
|
-
speak(sessionId, replyFor(language));
|
|
132
295
|
return;
|
|
133
296
|
}
|
|
134
297
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
language,
|
|
138
|
-
voice: language,
|
|
139
|
-
});
|
|
140
|
-
logSwitch(sessionId, "tts", tts);
|
|
298
|
+
// A switch is already waiting for the wait message or in flight.
|
|
299
|
+
if (state.pendingSwitch || state.switching) return;
|
|
141
300
|
|
|
142
|
-
|
|
143
|
-
scope: "stt",
|
|
144
|
-
language,
|
|
145
|
-
stt: language,
|
|
146
|
-
});
|
|
147
|
-
logSwitch(sessionId, "stt", stt);
|
|
301
|
+
prepareLanguageTransition(state);
|
|
148
302
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
303
|
+
// The runner plays nothing for manual switches and the target pools may be
|
|
304
|
+
// cold, so tell the user first, in the language being left.
|
|
305
|
+
const previous = state.language;
|
|
306
|
+
speak(sessionId, pickMessage(WAIT_MESSAGES, state.waitOverrides, previous));
|
|
153
307
|
|
|
154
|
-
|
|
155
|
-
|
|
308
|
+
// `speak` is fire-and-forget and the runner swaps the TTS without draining
|
|
309
|
+
// it, so the switch must wait until the wait message has been spoken
|
|
310
|
+
// (`agent_speaking_end`, delivered to onSpeechEvent). Inbound handlers of
|
|
311
|
+
// one session run strictly in order, so this handler must return instead
|
|
312
|
+
// of awaiting that event. The bounded timer covers a missing event.
|
|
313
|
+
const timer = setTimeout(() => {
|
|
314
|
+
agentLog(
|
|
315
|
+
"warn",
|
|
316
|
+
`language-switch ${sessionId} no agent_speaking_end within ${WAIT_SPEECH_TIMEOUT_MS}ms; switching anyway`,
|
|
317
|
+
);
|
|
318
|
+
startSwitch(sessionId, state);
|
|
319
|
+
}, WAIT_SPEECH_TIMEOUT_MS);
|
|
320
|
+
state.pendingSwitch = { language, previous, timer };
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
onSpeechEvent({ sessionId }, event) {
|
|
324
|
+
if (event.type !== "agent_speaking_end") return;
|
|
325
|
+
const state = sessions.get(sessionId);
|
|
326
|
+
if (state?.pendingSwitch) startSwitch(sessionId, state);
|
|
156
327
|
},
|
|
157
328
|
|
|
158
329
|
async onVoiceLanguageChanged({ sessionId, language }) {
|
|
@@ -214,6 +385,12 @@ defineAgent({
|
|
|
214
385
|
|
|
215
386
|
onUserSpeechFinal({ sessionId, text }) {
|
|
216
387
|
const state = stateFor(sessionId);
|
|
388
|
+
if (state.runnerAutoSwitch) {
|
|
389
|
+
// Reply immediately: the runner already holds the final until LID has
|
|
390
|
+
// decided, so no extra wait is needed to avoid a wrong-language echo.
|
|
391
|
+
speak(sessionId, `${echoPrefixFor(state.language)} ${text}`.trim());
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
217
394
|
if (state.suppressNextFinal) {
|
|
218
395
|
state.suppressNextFinal = false;
|
|
219
396
|
return;
|
|
@@ -228,6 +405,7 @@ defineAgent({
|
|
|
228
405
|
onSessionEnd({ sessionId }) {
|
|
229
406
|
const state = sessions.get(sessionId);
|
|
230
407
|
if (state?.echoTimer) clearTimeout(state.echoTimer);
|
|
408
|
+
if (state?.pendingSwitch) clearTimeout(state.pendingSwitch.timer);
|
|
231
409
|
sessions.delete(sessionId);
|
|
232
410
|
},
|
|
233
411
|
});
|