@voicethere/agent 0.9.2 → 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.
|
@@ -952,6 +952,50 @@ var ECHO_PREFIXES = {
|
|
|
952
952
|
pl: "Powiedzia\u0142e\u015B:",
|
|
953
953
|
ru: "\u0412\u044B \u0441\u043A\u0430\u0437\u0430\u043B\u0438:"
|
|
954
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
|
+
}
|
|
955
999
|
var REPLIES = {
|
|
956
1000
|
de: "Guten Tag. Ich antworte jetzt auf Deutsch.",
|
|
957
1001
|
es: "Hola. Ahora respondo en espa\xF1ol.",
|
|
@@ -970,7 +1014,11 @@ function stateFor(sessionId, env) {
|
|
|
970
1014
|
language: "en",
|
|
971
1015
|
runnerAutoSwitch: env ? isRunnerLidAutoSwitchEnabled(env) : false,
|
|
972
1016
|
echoTimer: void 0,
|
|
973
|
-
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])
|
|
974
1022
|
};
|
|
975
1023
|
sessions.set(sessionId, created);
|
|
976
1024
|
return created;
|
|
@@ -996,6 +1044,44 @@ function logSwitch(sessionId, side, result) {
|
|
|
996
1044
|
`voice ${side} ${sessionId} ${result.language ?? ""} provider=${provider ?? "unchanged"} model=${model ?? "unchanged"}`
|
|
997
1045
|
);
|
|
998
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
|
+
}
|
|
999
1085
|
function prepareLanguageTransition(state) {
|
|
1000
1086
|
if (state.echoTimer) {
|
|
1001
1087
|
clearTimeout(state.echoTimer);
|
|
@@ -1030,25 +1116,23 @@ defineAgent({
|
|
|
1030
1116
|
state.language = language;
|
|
1031
1117
|
return;
|
|
1032
1118
|
}
|
|
1119
|
+
if (state.pendingSwitch || state.switching) return;
|
|
1033
1120
|
prepareLanguageTransition(state);
|
|
1034
|
-
const
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
if (
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
}
|
|
1050
|
-
state.language = language;
|
|
1051
|
-
speak(sessionId, replyFor(language));
|
|
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);
|
|
1052
1136
|
},
|
|
1053
1137
|
async onVoiceLanguageChanged({ sessionId, language }) {
|
|
1054
1138
|
const state = stateFor(sessionId);
|
|
@@ -1124,6 +1208,16 @@ defineAgent({
|
|
|
1124
1208
|
onSessionEnd({ sessionId }) {
|
|
1125
1209
|
const state = sessions.get(sessionId);
|
|
1126
1210
|
if (state?.echoTimer) clearTimeout(state.echoTimer);
|
|
1211
|
+
if (state?.pendingSwitch) clearTimeout(state.pendingSwitch.timer);
|
|
1127
1212
|
sessions.delete(sessionId);
|
|
1128
1213
|
}
|
|
1129
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
|
+
};
|
package/package.json
CHANGED
|
@@ -9,12 +9,23 @@ 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
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.
|
|
17
26
|
|
|
27
|
+
In this mode the runner owns the wait message and the agent speaks no wait or fallback text of its own.
|
|
28
|
+
|
|
18
29
|
Detection and chat commands are unchanged: `/tts` and `/stt` still call `setVoiceLanguage` for one vendor at a time.
|
|
19
30
|
|
|
20
31
|
Chat commands change one vendor while the session stays connected. API keys are project secrets on the running deploy, not arguments:
|
|
@@ -4,7 +4,17 @@
|
|
|
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.
|
|
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).
|
|
8
18
|
* (B) Project enables runner auto-switch — STT/TTS are runner-owned. The
|
|
9
19
|
* runner replays the utterance into the new language's STT, so the next
|
|
10
20
|
* final is the correctly recognized text. The agent never calls
|
|
@@ -47,6 +57,70 @@ const ECHO_PREFIXES: Record<string, string> = {
|
|
|
47
57
|
ru: "Вы сказали:",
|
|
48
58
|
};
|
|
49
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
|
+
|
|
50
124
|
const REPLIES: Record<string, string> = {
|
|
51
125
|
de: "Guten Tag. Ich antworte jetzt auf Deutsch.",
|
|
52
126
|
es: "Hola. Ahora respondo en español.",
|
|
@@ -63,6 +137,16 @@ type SessionLanguage = {
|
|
|
63
137
|
runnerAutoSwitch: boolean;
|
|
64
138
|
echoTimer: ReturnType<typeof setTimeout> | undefined;
|
|
65
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>;
|
|
66
150
|
};
|
|
67
151
|
|
|
68
152
|
const sessions = new Map<string, SessionLanguage>();
|
|
@@ -78,6 +162,10 @@ function stateFor(
|
|
|
78
162
|
runnerAutoSwitch: env ? isRunnerLidAutoSwitchEnabled(env) : false,
|
|
79
163
|
echoTimer: undefined,
|
|
80
164
|
suppressNextFinal: false,
|
|
165
|
+
pendingSwitch: undefined,
|
|
166
|
+
switching: false,
|
|
167
|
+
waitOverrides: parseMessageOverrides(env?.[WAIT_MESSAGES_ENV]),
|
|
168
|
+
failedOverrides: parseMessageOverrides(env?.[FAILED_MESSAGES_ENV]),
|
|
81
169
|
};
|
|
82
170
|
sessions.set(sessionId, created);
|
|
83
171
|
return created;
|
|
@@ -111,6 +199,61 @@ function logSwitch(
|
|
|
111
199
|
);
|
|
112
200
|
}
|
|
113
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
|
+
|
|
114
257
|
function prepareLanguageTransition(state: SessionLanguage): void {
|
|
115
258
|
if (state.echoTimer) {
|
|
116
259
|
clearTimeout(state.echoTimer);
|
|
@@ -152,29 +295,35 @@ defineAgent({
|
|
|
152
295
|
return;
|
|
153
296
|
}
|
|
154
297
|
|
|
155
|
-
|
|
298
|
+
// A switch is already waiting for the wait message or in flight.
|
|
299
|
+
if (state.pendingSwitch || state.switching) return;
|
|
156
300
|
|
|
157
|
-
|
|
158
|
-
scope: "tts",
|
|
159
|
-
language,
|
|
160
|
-
voice: language,
|
|
161
|
-
});
|
|
162
|
-
logSwitch(sessionId, "tts", tts);
|
|
301
|
+
prepareLanguageTransition(state);
|
|
163
302
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
});
|
|
169
|
-
logSwitch(sessionId, "stt", stt);
|
|
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));
|
|
170
307
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
+
},
|
|
175
322
|
|
|
176
|
-
|
|
177
|
-
|
|
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);
|
|
178
327
|
},
|
|
179
328
|
|
|
180
329
|
async onVoiceLanguageChanged({ sessionId, language }) {
|
|
@@ -256,6 +405,7 @@ defineAgent({
|
|
|
256
405
|
onSessionEnd({ sessionId }) {
|
|
257
406
|
const state = sessions.get(sessionId);
|
|
258
407
|
if (state?.echoTimer) clearTimeout(state.echoTimer);
|
|
408
|
+
if (state?.pendingSwitch) clearTimeout(state.pendingSwitch.timer);
|
|
259
409
|
sessions.delete(sessionId);
|
|
260
410
|
},
|
|
261
411
|
});
|