@absolutejs/voice 0.0.22-beta.625 → 0.0.22-beta.627
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/core/numberNormalizer.d.ts +1 -0
- package/dist/core/types.d.ts +8 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +218 -4
- package/dist/testing/index.js +201 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const normalizeSpokenNumbers: (input: string) => string;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -46,6 +46,11 @@ export type VoiceLanguageStrategy = {
|
|
|
46
46
|
primaryLanguage?: string;
|
|
47
47
|
secondaryLanguages: string[];
|
|
48
48
|
};
|
|
49
|
+
export type VoiceLanguageStrategyResolver<TContext = unknown> = (input: {
|
|
50
|
+
context: TContext;
|
|
51
|
+
scenarioId?: string;
|
|
52
|
+
sessionId: string;
|
|
53
|
+
}) => Promise<VoiceLanguageStrategy | void> | VoiceLanguageStrategy | void;
|
|
49
54
|
export type VoicePhraseHint = {
|
|
50
55
|
text: string;
|
|
51
56
|
aliases?: string[];
|
|
@@ -825,7 +830,7 @@ export type VoicePluginConfig<TContext = unknown, TSession extends VoiceSessionR
|
|
|
825
830
|
}) => string | Promise<string>);
|
|
826
831
|
maxReprompts?: number;
|
|
827
832
|
};
|
|
828
|
-
languageStrategy?: VoiceLanguageStrategy
|
|
833
|
+
languageStrategy?: VoiceLanguageStrategy | VoiceLanguageStrategyResolver<TContext>;
|
|
829
834
|
lexicon?: VoiceLexiconEntry[] | VoiceLexiconResolver<TContext>;
|
|
830
835
|
phraseHints?: VoicePhraseHint[] | VoicePhraseHintResolver<TContext>;
|
|
831
836
|
preset?: VoiceRuntimePreset;
|
|
@@ -861,6 +866,7 @@ export type VoicePluginConfig<TContext = unknown, TSession extends VoiceSessionR
|
|
|
861
866
|
defaultSilentTurnAck?: string;
|
|
862
867
|
routeOnTurnTimeoutMs?: number;
|
|
863
868
|
audioConditioning?: VoiceAudioConditioningConfig;
|
|
869
|
+
normalizeNumbers?: boolean;
|
|
864
870
|
noiseSuppressor?: VoiceNoiseSuppressor;
|
|
865
871
|
noiseSuppressorFormat?: AudioFormat;
|
|
866
872
|
logger?: VoiceLogger;
|
|
@@ -1131,6 +1137,7 @@ export type CreateVoiceSessionOptions<TContext = unknown, TSession extends Voice
|
|
|
1131
1137
|
sttLifecycle: VoiceSTTLifecycle;
|
|
1132
1138
|
turnDetection: VoiceResolvedTurnDetectionConfig;
|
|
1133
1139
|
audioConditioning?: VoiceResolvedAudioConditioningConfig;
|
|
1140
|
+
normalizeNumbers?: boolean;
|
|
1134
1141
|
noiseSuppressor?: VoiceNoiseSuppressor;
|
|
1135
1142
|
noiseSuppressorFormat?: AudioFormat;
|
|
1136
1143
|
handoff?: VoiceHandoffConfig<TContext, TSession, TResult>;
|
package/dist/index.d.ts
CHANGED
|
@@ -77,6 +77,7 @@ export { createVoiceMCPToolset } from "./core/mcpToolset";
|
|
|
77
77
|
export type { CreateVoiceMCPToolsetOptions, MCPClientLike, MCPToolCallResult, MCPToolContentBlock, MCPToolDefinition, VoiceMCPToolResult, } from "./core/mcpToolset";
|
|
78
78
|
export { createAIVoiceModel } from "./core/aiVoiceModel";
|
|
79
79
|
export type { CreateAIVoiceModelOptions } from "./core/aiVoiceModel";
|
|
80
|
+
export { normalizeSpokenNumbers } from "./core/numberNormalizer";
|
|
80
81
|
export { createVoiceAIJudgeCompletion, createVoiceLLMJudge, } from "./core/llmJudge";
|
|
81
82
|
export type { CreateVoiceAIJudgeCompletionOptions, CreateVoiceLLMJudgeOptions, VoiceLLMJudge, VoiceLLMJudgeCompletion, VoiceLLMJudgeCriterionVerdict, VoiceLLMJudgeInput, VoiceLLMJudgeRubric, VoiceLLMJudgeRubricCriterion, VoiceLLMJudgeVerdict, } from "./core/llmJudge";
|
|
82
83
|
export { DEFAULT_VOICE_REDACTION_PATTERNS, createVoiceTranscriptRedactor, redactVoiceTranscript, } from "./core/redaction";
|
package/dist/index.js
CHANGED
|
@@ -3096,6 +3096,191 @@ var toVoiceSessionSummary = (session) => ({
|
|
|
3096
3096
|
// src/core/session.ts
|
|
3097
3097
|
import { Buffer as Buffer2 } from "buffer";
|
|
3098
3098
|
|
|
3099
|
+
// src/core/numberNormalizer.ts
|
|
3100
|
+
var ONES = {
|
|
3101
|
+
eight: 8,
|
|
3102
|
+
eighteen: 18,
|
|
3103
|
+
eleven: 11,
|
|
3104
|
+
fifteen: 15,
|
|
3105
|
+
five: 5,
|
|
3106
|
+
four: 4,
|
|
3107
|
+
fourteen: 14,
|
|
3108
|
+
nine: 9,
|
|
3109
|
+
nineteen: 19,
|
|
3110
|
+
one: 1,
|
|
3111
|
+
seven: 7,
|
|
3112
|
+
seventeen: 17,
|
|
3113
|
+
six: 6,
|
|
3114
|
+
sixteen: 16,
|
|
3115
|
+
ten: 10,
|
|
3116
|
+
thirteen: 13,
|
|
3117
|
+
three: 3,
|
|
3118
|
+
twelve: 12,
|
|
3119
|
+
two: 2,
|
|
3120
|
+
zero: 0
|
|
3121
|
+
};
|
|
3122
|
+
var TENS = {
|
|
3123
|
+
eighty: 80,
|
|
3124
|
+
fifty: 50,
|
|
3125
|
+
forty: 40,
|
|
3126
|
+
ninety: 90,
|
|
3127
|
+
seventy: 70,
|
|
3128
|
+
sixty: 60,
|
|
3129
|
+
thirty: 30,
|
|
3130
|
+
twenty: 20
|
|
3131
|
+
};
|
|
3132
|
+
var SCALES = {
|
|
3133
|
+
billion: 1e9,
|
|
3134
|
+
million: 1e6,
|
|
3135
|
+
thousand: 1000,
|
|
3136
|
+
trillion: 1000000000000
|
|
3137
|
+
};
|
|
3138
|
+
var MAGNITUDE_WORDS = [
|
|
3139
|
+
[1000000000000, "trillion"],
|
|
3140
|
+
[1e9, "billion"],
|
|
3141
|
+
[1e6, "million"]
|
|
3142
|
+
];
|
|
3143
|
+
var FILLER = new Set(["and", "a", "an"]);
|
|
3144
|
+
var DECIMAL_PLACES = 3;
|
|
3145
|
+
var isValueWord = (word) => (word in ONES) || (word in TENS) || word === "hundred";
|
|
3146
|
+
var isScaleWord = (word) => (word in SCALES);
|
|
3147
|
+
var isNumberWord = (word) => isValueWord(word) || isScaleWord(word) || word === "point" || FILLER.has(word);
|
|
3148
|
+
var trimNumber = (value) => {
|
|
3149
|
+
if (Number.isInteger(value))
|
|
3150
|
+
return value.toLocaleString("en-US");
|
|
3151
|
+
return String(Number(value.toFixed(DECIMAL_PLACES)));
|
|
3152
|
+
};
|
|
3153
|
+
var renderValue = (value, usedMagnitude) => {
|
|
3154
|
+
if (usedMagnitude) {
|
|
3155
|
+
for (const [scale, word] of MAGNITUDE_WORDS) {
|
|
3156
|
+
if (value >= scale) {
|
|
3157
|
+
const scaled = value / scale;
|
|
3158
|
+
if (Number(scaled.toFixed(DECIMAL_PLACES)) === scaled) {
|
|
3159
|
+
return `${trimNumber(scaled)} ${word}`;
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
return trimNumber(value);
|
|
3165
|
+
};
|
|
3166
|
+
var parseNumberWords = (words) => {
|
|
3167
|
+
let total = 0;
|
|
3168
|
+
let current = 0;
|
|
3169
|
+
let usedMagnitude = false;
|
|
3170
|
+
let sawNumber = false;
|
|
3171
|
+
let decimal = null;
|
|
3172
|
+
const foldDecimal = () => {
|
|
3173
|
+
if (decimal && decimal.length > 0)
|
|
3174
|
+
current += Number(`0.${decimal}`);
|
|
3175
|
+
decimal = null;
|
|
3176
|
+
};
|
|
3177
|
+
for (const word of words) {
|
|
3178
|
+
if (word === "point") {
|
|
3179
|
+
decimal = "";
|
|
3180
|
+
continue;
|
|
3181
|
+
}
|
|
3182
|
+
const one = ONES[word];
|
|
3183
|
+
const ten = TENS[word];
|
|
3184
|
+
const scale = SCALES[word];
|
|
3185
|
+
if (decimal !== null) {
|
|
3186
|
+
if (one !== undefined && one <= 9) {
|
|
3187
|
+
decimal += String(one);
|
|
3188
|
+
sawNumber = true;
|
|
3189
|
+
continue;
|
|
3190
|
+
}
|
|
3191
|
+
foldDecimal();
|
|
3192
|
+
}
|
|
3193
|
+
if (FILLER.has(word))
|
|
3194
|
+
continue;
|
|
3195
|
+
if (one !== undefined) {
|
|
3196
|
+
current += one;
|
|
3197
|
+
sawNumber = true;
|
|
3198
|
+
} else if (ten !== undefined) {
|
|
3199
|
+
current += ten;
|
|
3200
|
+
sawNumber = true;
|
|
3201
|
+
} else if (word === "hundred") {
|
|
3202
|
+
current = (current === 0 ? 1 : current) * 100;
|
|
3203
|
+
sawNumber = true;
|
|
3204
|
+
} else if (scale !== undefined) {
|
|
3205
|
+
total += (current === 0 ? 1 : current) * scale;
|
|
3206
|
+
current = 0;
|
|
3207
|
+
sawNumber = true;
|
|
3208
|
+
usedMagnitude = true;
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
foldDecimal();
|
|
3212
|
+
if (!sawNumber)
|
|
3213
|
+
return null;
|
|
3214
|
+
return { usedMagnitude, value: total + current };
|
|
3215
|
+
};
|
|
3216
|
+
var PERCENT_RE = /^(per ?cent|percent|percentage)$/;
|
|
3217
|
+
var CURRENCY_RE = /^(dollars?|bucks?|usd)$/;
|
|
3218
|
+
var WORD_RE = /^[A-Za-z]+(?:-[A-Za-z]+)*$/;
|
|
3219
|
+
var wordsOf = (token) => token.toLowerCase().split("-");
|
|
3220
|
+
var isNumberToken = (token) => WORD_RE.test(token) && wordsOf(token).every(isNumberWord);
|
|
3221
|
+
var startsNumber = (token) => WORD_RE.test(token) && wordsOf(token).some(isValueWord);
|
|
3222
|
+
var isSpace = (token) => /^\s+$/.test(token);
|
|
3223
|
+
var normalizeSpokenNumbers = (input) => {
|
|
3224
|
+
if (!input)
|
|
3225
|
+
return input;
|
|
3226
|
+
const parts = input.match(/[A-Za-z]+(?:-[A-Za-z]+)*|[^A-Za-z]+/g);
|
|
3227
|
+
if (!parts)
|
|
3228
|
+
return input;
|
|
3229
|
+
const at = (idx) => parts[idx] ?? "";
|
|
3230
|
+
const out = [];
|
|
3231
|
+
let i = 0;
|
|
3232
|
+
while (i < parts.length) {
|
|
3233
|
+
const token = at(i);
|
|
3234
|
+
const lower = token.toLowerCase();
|
|
3235
|
+
if (lower === "a" || lower === "an") {
|
|
3236
|
+
const nextWord = at(i + 2);
|
|
3237
|
+
const nextHead = wordsOf(nextWord)[0] ?? "";
|
|
3238
|
+
const nextIsScale = isSpace(at(i + 1)) && WORD_RE.test(nextWord) && (nextHead === "hundred" || isScaleWord(nextHead));
|
|
3239
|
+
if (!nextIsScale) {
|
|
3240
|
+
out.push(token);
|
|
3241
|
+
i += 1;
|
|
3242
|
+
continue;
|
|
3243
|
+
}
|
|
3244
|
+
} else if (!startsNumber(token)) {
|
|
3245
|
+
out.push(token);
|
|
3246
|
+
i += 1;
|
|
3247
|
+
continue;
|
|
3248
|
+
}
|
|
3249
|
+
const spanIdx = [i];
|
|
3250
|
+
let j = i + 1;
|
|
3251
|
+
while (isSpace(at(j)) && isNumberToken(at(j + 1))) {
|
|
3252
|
+
spanIdx.push(j + 1);
|
|
3253
|
+
j += 2;
|
|
3254
|
+
}
|
|
3255
|
+
const words = spanIdx.flatMap((k) => wordsOf(at(k)));
|
|
3256
|
+
while (words.length > 0 && FILLER.has(words[words.length - 1] ?? "")) {
|
|
3257
|
+
words.pop();
|
|
3258
|
+
}
|
|
3259
|
+
const parsed = parseNumberWords(words);
|
|
3260
|
+
if (!parsed) {
|
|
3261
|
+
out.push(token);
|
|
3262
|
+
i += 1;
|
|
3263
|
+
continue;
|
|
3264
|
+
}
|
|
3265
|
+
let rendered = renderValue(parsed.value, parsed.usedMagnitude);
|
|
3266
|
+
let lastIdx = spanIdx[spanIdx.length - 1] ?? i;
|
|
3267
|
+
const unitWord = at(lastIdx + 2);
|
|
3268
|
+
if (isSpace(at(lastIdx + 1)) && WORD_RE.test(unitWord)) {
|
|
3269
|
+
const unit = unitWord.toLowerCase();
|
|
3270
|
+
if (PERCENT_RE.test(unit)) {
|
|
3271
|
+
rendered = `${rendered}%`;
|
|
3272
|
+
lastIdx += 2;
|
|
3273
|
+
} else if (CURRENCY_RE.test(unit)) {
|
|
3274
|
+
rendered = rendered.startsWith("$") ? rendered : `$${rendered}`;
|
|
3275
|
+
lastIdx += 2;
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
out.push(rendered);
|
|
3279
|
+
i = lastIdx + 1;
|
|
3280
|
+
}
|
|
3281
|
+
return out.join("");
|
|
3282
|
+
};
|
|
3283
|
+
|
|
3099
3284
|
// src/core/debugTiming.ts
|
|
3100
3285
|
var timingEnabled = () => process.env.ABSOLUTEJS_VOICE_TIMING === "1" || process.env.ABSOLUTEJS_VOICE_TIMING === "true";
|
|
3101
3286
|
var emitTiming = (sessionId, stage, elapsedMs, detail) => {
|
|
@@ -5834,6 +6019,9 @@ var createVoiceSession = (options) => {
|
|
|
5834
6019
|
};
|
|
5835
6020
|
const completeTurn = async (session, turn) => {
|
|
5836
6021
|
console.error(`[voice] completeTurn ENTER session=${options.id} turn=${turn.id} textLen=${turn.text?.length ?? 0}`);
|
|
6022
|
+
if (options.normalizeNumbers && turn.text) {
|
|
6023
|
+
turn.text = normalizeSpokenNumbers(turn.text);
|
|
6024
|
+
}
|
|
5837
6025
|
kickStuckCloseWatchdog();
|
|
5838
6026
|
kickIdleRepromptWatchdog(true);
|
|
5839
6027
|
const liveOpsControl = await options.liveOps?.getControl(options.id);
|
|
@@ -25430,6 +25618,16 @@ var createTwilioMediaStreamBridge = (socket, options) => {
|
|
|
25430
25618
|
}
|
|
25431
25619
|
return normalizePhraseHints(options.phraseHints);
|
|
25432
25620
|
};
|
|
25621
|
+
const resolveLanguageStrategy = async () => {
|
|
25622
|
+
if (typeof options.languageStrategy === "function") {
|
|
25623
|
+
return await options.languageStrategy({
|
|
25624
|
+
context: options.context,
|
|
25625
|
+
scenarioId: bridgeState.scenarioId ?? undefined,
|
|
25626
|
+
sessionId: bridgeState.sessionId ?? ""
|
|
25627
|
+
}) ?? undefined;
|
|
25628
|
+
}
|
|
25629
|
+
return options.languageStrategy;
|
|
25630
|
+
};
|
|
25433
25631
|
const ensureSession = async () => {
|
|
25434
25632
|
if (sessionHandle) {
|
|
25435
25633
|
return sessionHandle;
|
|
@@ -25437,6 +25635,7 @@ var createTwilioMediaStreamBridge = (socket, options) => {
|
|
|
25437
25635
|
bridgeState.sessionId ??= `phone-${Date.now().toString(36)}`;
|
|
25438
25636
|
const lexicon = await resolveLexicon();
|
|
25439
25637
|
const phraseHints = await resolvePhraseHints();
|
|
25638
|
+
const languageStrategy = await resolveLanguageStrategy();
|
|
25440
25639
|
const normalizedOnTurn = normalizeOnTurn(options.onTurn);
|
|
25441
25640
|
const route = {
|
|
25442
25641
|
correctTurn: options.correctTurn,
|
|
@@ -25468,13 +25667,14 @@ var createTwilioMediaStreamBridge = (socket, options) => {
|
|
|
25468
25667
|
const voiceSocket = createTwilioSocketAdapter(socket, () => bridgeState);
|
|
25469
25668
|
sessionHandle = createVoiceSession({
|
|
25470
25669
|
audioConditioning,
|
|
25670
|
+
normalizeNumbers: options.normalizeNumbers,
|
|
25471
25671
|
context: options.context,
|
|
25472
25672
|
costAccountant: options.costAccountant,
|
|
25473
25673
|
costTelemetry: options.costTelemetry,
|
|
25474
25674
|
costTelephony: options.costTelephony,
|
|
25475
25675
|
greeting: options.greeting,
|
|
25476
25676
|
id: bridgeState.sessionId,
|
|
25477
|
-
languageStrategy
|
|
25677
|
+
languageStrategy,
|
|
25478
25678
|
lexicon,
|
|
25479
25679
|
logger,
|
|
25480
25680
|
phraseHints,
|
|
@@ -39586,6 +39786,7 @@ var resolveSessionOptions = (config) => {
|
|
|
39586
39786
|
const preset = resolveVoiceRuntimePreset(config.preset);
|
|
39587
39787
|
return {
|
|
39588
39788
|
audioConditioning: config.audioConditioning !== undefined ? resolveAudioConditioningConfig(config.audioConditioning) : preset.audioConditioning,
|
|
39789
|
+
normalizeNumbers: config.normalizeNumbers,
|
|
39589
39790
|
noiseSuppressor: config.noiseSuppressor,
|
|
39590
39791
|
noiseSuppressorFormat: config.noiseSuppressorFormat,
|
|
39591
39792
|
costTelemetry: config.costTelemetry,
|
|
@@ -39633,6 +39834,12 @@ var resolveLexicon = async (config, input) => {
|
|
|
39633
39834
|
}
|
|
39634
39835
|
return normalizeLexicon2(config.lexicon);
|
|
39635
39836
|
};
|
|
39837
|
+
var resolveLanguageStrategy = async (config, input) => {
|
|
39838
|
+
if (typeof config.languageStrategy === "function") {
|
|
39839
|
+
return await config.languageStrategy(input) ?? undefined;
|
|
39840
|
+
}
|
|
39841
|
+
return config.languageStrategy;
|
|
39842
|
+
};
|
|
39636
39843
|
var resolveProfileSwitchGuard = async (config, runtime, input) => {
|
|
39637
39844
|
const guard = config.profileSwitchGuard;
|
|
39638
39845
|
if (!guard || runtime.profileSwitchGuardedSessions.has(input.sessionId)) {
|
|
@@ -39797,8 +40004,14 @@ var voice = (config) => {
|
|
|
39797
40004
|
scenarioId,
|
|
39798
40005
|
sessionId
|
|
39799
40006
|
});
|
|
40007
|
+
const languageStrategy = await resolveLanguageStrategy(config, {
|
|
40008
|
+
context,
|
|
40009
|
+
scenarioId,
|
|
40010
|
+
sessionId
|
|
40011
|
+
});
|
|
39800
40012
|
return createVoiceSession({
|
|
39801
40013
|
audioConditioning: sessionOptions.audioConditioning,
|
|
40014
|
+
normalizeNumbers: sessionOptions.normalizeNumbers,
|
|
39802
40015
|
noiseSuppressor: sessionOptions.noiseSuppressor,
|
|
39803
40016
|
noiseSuppressorFormat: sessionOptions.noiseSuppressorFormat,
|
|
39804
40017
|
context,
|
|
@@ -39809,7 +40022,7 @@ var voice = (config) => {
|
|
|
39809
40022
|
stuckCallClose: config.stuckCallClose,
|
|
39810
40023
|
idleReprompt: config.idleReprompt,
|
|
39811
40024
|
handoff: config.handoff,
|
|
39812
|
-
languageStrategy
|
|
40025
|
+
languageStrategy,
|
|
39813
40026
|
lexicon,
|
|
39814
40027
|
liveOps: config.liveOps,
|
|
39815
40028
|
logger: sessionOptions.logger,
|
|
@@ -50973,7 +51186,7 @@ var DEFAULT_VOICE_CAMPAIGN_TEMPLATE_FILTERS = {
|
|
|
50973
51186
|
ssml: (value) => escapeSpeech(String(value ?? "")),
|
|
50974
51187
|
upper: (value) => String(value ?? "").toUpperCase()
|
|
50975
51188
|
};
|
|
50976
|
-
var
|
|
51189
|
+
var renderValue2 = (value, filters, filterChain) => {
|
|
50977
51190
|
let cursor = value;
|
|
50978
51191
|
for (const segment of filterChain) {
|
|
50979
51192
|
const parts2 = segment.split(":");
|
|
@@ -51018,7 +51231,7 @@ var resolveVoiceCampaignTemplate = (template, options) => {
|
|
|
51018
51231
|
}
|
|
51019
51232
|
return options.fallback ?? "";
|
|
51020
51233
|
}
|
|
51021
|
-
return
|
|
51234
|
+
return renderValue2(value, filters, segments.slice(1));
|
|
51022
51235
|
});
|
|
51023
51236
|
return { missingVariables: Array.from(missing), output };
|
|
51024
51237
|
};
|
|
@@ -53651,6 +53864,7 @@ export {
|
|
|
53651
53864
|
parseVoiceSessionSnapshot,
|
|
53652
53865
|
parseVoiceAIScorecardResponse,
|
|
53653
53866
|
normalizeVoiceProofTrendReport,
|
|
53867
|
+
normalizeSpokenNumbers,
|
|
53654
53868
|
normalizePhoneNumber,
|
|
53655
53869
|
muteVoiceMonitorIssue,
|
|
53656
53870
|
matchesVoiceOpsTaskAssignmentRule,
|
package/dist/testing/index.js
CHANGED
|
@@ -5536,6 +5536,191 @@ var createVoiceMemoryStore = () => {
|
|
|
5536
5536
|
// src/core/session.ts
|
|
5537
5537
|
import { Buffer as Buffer2 } from "buffer";
|
|
5538
5538
|
|
|
5539
|
+
// src/core/numberNormalizer.ts
|
|
5540
|
+
var ONES = {
|
|
5541
|
+
eight: 8,
|
|
5542
|
+
eighteen: 18,
|
|
5543
|
+
eleven: 11,
|
|
5544
|
+
fifteen: 15,
|
|
5545
|
+
five: 5,
|
|
5546
|
+
four: 4,
|
|
5547
|
+
fourteen: 14,
|
|
5548
|
+
nine: 9,
|
|
5549
|
+
nineteen: 19,
|
|
5550
|
+
one: 1,
|
|
5551
|
+
seven: 7,
|
|
5552
|
+
seventeen: 17,
|
|
5553
|
+
six: 6,
|
|
5554
|
+
sixteen: 16,
|
|
5555
|
+
ten: 10,
|
|
5556
|
+
thirteen: 13,
|
|
5557
|
+
three: 3,
|
|
5558
|
+
twelve: 12,
|
|
5559
|
+
two: 2,
|
|
5560
|
+
zero: 0
|
|
5561
|
+
};
|
|
5562
|
+
var TENS = {
|
|
5563
|
+
eighty: 80,
|
|
5564
|
+
fifty: 50,
|
|
5565
|
+
forty: 40,
|
|
5566
|
+
ninety: 90,
|
|
5567
|
+
seventy: 70,
|
|
5568
|
+
sixty: 60,
|
|
5569
|
+
thirty: 30,
|
|
5570
|
+
twenty: 20
|
|
5571
|
+
};
|
|
5572
|
+
var SCALES = {
|
|
5573
|
+
billion: 1e9,
|
|
5574
|
+
million: 1e6,
|
|
5575
|
+
thousand: 1000,
|
|
5576
|
+
trillion: 1000000000000
|
|
5577
|
+
};
|
|
5578
|
+
var MAGNITUDE_WORDS = [
|
|
5579
|
+
[1000000000000, "trillion"],
|
|
5580
|
+
[1e9, "billion"],
|
|
5581
|
+
[1e6, "million"]
|
|
5582
|
+
];
|
|
5583
|
+
var FILLER = new Set(["and", "a", "an"]);
|
|
5584
|
+
var DECIMAL_PLACES = 3;
|
|
5585
|
+
var isValueWord = (word) => (word in ONES) || (word in TENS) || word === "hundred";
|
|
5586
|
+
var isScaleWord = (word) => (word in SCALES);
|
|
5587
|
+
var isNumberWord = (word) => isValueWord(word) || isScaleWord(word) || word === "point" || FILLER.has(word);
|
|
5588
|
+
var trimNumber = (value) => {
|
|
5589
|
+
if (Number.isInteger(value))
|
|
5590
|
+
return value.toLocaleString("en-US");
|
|
5591
|
+
return String(Number(value.toFixed(DECIMAL_PLACES)));
|
|
5592
|
+
};
|
|
5593
|
+
var renderValue = (value, usedMagnitude) => {
|
|
5594
|
+
if (usedMagnitude) {
|
|
5595
|
+
for (const [scale, word] of MAGNITUDE_WORDS) {
|
|
5596
|
+
if (value >= scale) {
|
|
5597
|
+
const scaled = value / scale;
|
|
5598
|
+
if (Number(scaled.toFixed(DECIMAL_PLACES)) === scaled) {
|
|
5599
|
+
return `${trimNumber(scaled)} ${word}`;
|
|
5600
|
+
}
|
|
5601
|
+
}
|
|
5602
|
+
}
|
|
5603
|
+
}
|
|
5604
|
+
return trimNumber(value);
|
|
5605
|
+
};
|
|
5606
|
+
var parseNumberWords = (words) => {
|
|
5607
|
+
let total = 0;
|
|
5608
|
+
let current = 0;
|
|
5609
|
+
let usedMagnitude = false;
|
|
5610
|
+
let sawNumber = false;
|
|
5611
|
+
let decimal = null;
|
|
5612
|
+
const foldDecimal = () => {
|
|
5613
|
+
if (decimal && decimal.length > 0)
|
|
5614
|
+
current += Number(`0.${decimal}`);
|
|
5615
|
+
decimal = null;
|
|
5616
|
+
};
|
|
5617
|
+
for (const word of words) {
|
|
5618
|
+
if (word === "point") {
|
|
5619
|
+
decimal = "";
|
|
5620
|
+
continue;
|
|
5621
|
+
}
|
|
5622
|
+
const one = ONES[word];
|
|
5623
|
+
const ten = TENS[word];
|
|
5624
|
+
const scale = SCALES[word];
|
|
5625
|
+
if (decimal !== null) {
|
|
5626
|
+
if (one !== undefined && one <= 9) {
|
|
5627
|
+
decimal += String(one);
|
|
5628
|
+
sawNumber = true;
|
|
5629
|
+
continue;
|
|
5630
|
+
}
|
|
5631
|
+
foldDecimal();
|
|
5632
|
+
}
|
|
5633
|
+
if (FILLER.has(word))
|
|
5634
|
+
continue;
|
|
5635
|
+
if (one !== undefined) {
|
|
5636
|
+
current += one;
|
|
5637
|
+
sawNumber = true;
|
|
5638
|
+
} else if (ten !== undefined) {
|
|
5639
|
+
current += ten;
|
|
5640
|
+
sawNumber = true;
|
|
5641
|
+
} else if (word === "hundred") {
|
|
5642
|
+
current = (current === 0 ? 1 : current) * 100;
|
|
5643
|
+
sawNumber = true;
|
|
5644
|
+
} else if (scale !== undefined) {
|
|
5645
|
+
total += (current === 0 ? 1 : current) * scale;
|
|
5646
|
+
current = 0;
|
|
5647
|
+
sawNumber = true;
|
|
5648
|
+
usedMagnitude = true;
|
|
5649
|
+
}
|
|
5650
|
+
}
|
|
5651
|
+
foldDecimal();
|
|
5652
|
+
if (!sawNumber)
|
|
5653
|
+
return null;
|
|
5654
|
+
return { usedMagnitude, value: total + current };
|
|
5655
|
+
};
|
|
5656
|
+
var PERCENT_RE = /^(per ?cent|percent|percentage)$/;
|
|
5657
|
+
var CURRENCY_RE = /^(dollars?|bucks?|usd)$/;
|
|
5658
|
+
var WORD_RE = /^[A-Za-z]+(?:-[A-Za-z]+)*$/;
|
|
5659
|
+
var wordsOf = (token) => token.toLowerCase().split("-");
|
|
5660
|
+
var isNumberToken = (token) => WORD_RE.test(token) && wordsOf(token).every(isNumberWord);
|
|
5661
|
+
var startsNumber = (token) => WORD_RE.test(token) && wordsOf(token).some(isValueWord);
|
|
5662
|
+
var isSpace = (token) => /^\s+$/.test(token);
|
|
5663
|
+
var normalizeSpokenNumbers = (input) => {
|
|
5664
|
+
if (!input)
|
|
5665
|
+
return input;
|
|
5666
|
+
const parts = input.match(/[A-Za-z]+(?:-[A-Za-z]+)*|[^A-Za-z]+/g);
|
|
5667
|
+
if (!parts)
|
|
5668
|
+
return input;
|
|
5669
|
+
const at = (idx) => parts[idx] ?? "";
|
|
5670
|
+
const out = [];
|
|
5671
|
+
let i = 0;
|
|
5672
|
+
while (i < parts.length) {
|
|
5673
|
+
const token = at(i);
|
|
5674
|
+
const lower = token.toLowerCase();
|
|
5675
|
+
if (lower === "a" || lower === "an") {
|
|
5676
|
+
const nextWord = at(i + 2);
|
|
5677
|
+
const nextHead = wordsOf(nextWord)[0] ?? "";
|
|
5678
|
+
const nextIsScale = isSpace(at(i + 1)) && WORD_RE.test(nextWord) && (nextHead === "hundred" || isScaleWord(nextHead));
|
|
5679
|
+
if (!nextIsScale) {
|
|
5680
|
+
out.push(token);
|
|
5681
|
+
i += 1;
|
|
5682
|
+
continue;
|
|
5683
|
+
}
|
|
5684
|
+
} else if (!startsNumber(token)) {
|
|
5685
|
+
out.push(token);
|
|
5686
|
+
i += 1;
|
|
5687
|
+
continue;
|
|
5688
|
+
}
|
|
5689
|
+
const spanIdx = [i];
|
|
5690
|
+
let j = i + 1;
|
|
5691
|
+
while (isSpace(at(j)) && isNumberToken(at(j + 1))) {
|
|
5692
|
+
spanIdx.push(j + 1);
|
|
5693
|
+
j += 2;
|
|
5694
|
+
}
|
|
5695
|
+
const words = spanIdx.flatMap((k) => wordsOf(at(k)));
|
|
5696
|
+
while (words.length > 0 && FILLER.has(words[words.length - 1] ?? "")) {
|
|
5697
|
+
words.pop();
|
|
5698
|
+
}
|
|
5699
|
+
const parsed = parseNumberWords(words);
|
|
5700
|
+
if (!parsed) {
|
|
5701
|
+
out.push(token);
|
|
5702
|
+
i += 1;
|
|
5703
|
+
continue;
|
|
5704
|
+
}
|
|
5705
|
+
let rendered = renderValue(parsed.value, parsed.usedMagnitude);
|
|
5706
|
+
let lastIdx = spanIdx[spanIdx.length - 1] ?? i;
|
|
5707
|
+
const unitWord = at(lastIdx + 2);
|
|
5708
|
+
if (isSpace(at(lastIdx + 1)) && WORD_RE.test(unitWord)) {
|
|
5709
|
+
const unit = unitWord.toLowerCase();
|
|
5710
|
+
if (PERCENT_RE.test(unit)) {
|
|
5711
|
+
rendered = `${rendered}%`;
|
|
5712
|
+
lastIdx += 2;
|
|
5713
|
+
} else if (CURRENCY_RE.test(unit)) {
|
|
5714
|
+
rendered = rendered.startsWith("$") ? rendered : `$${rendered}`;
|
|
5715
|
+
lastIdx += 2;
|
|
5716
|
+
}
|
|
5717
|
+
}
|
|
5718
|
+
out.push(rendered);
|
|
5719
|
+
i = lastIdx + 1;
|
|
5720
|
+
}
|
|
5721
|
+
return out.join("");
|
|
5722
|
+
};
|
|
5723
|
+
|
|
5539
5724
|
// src/core/backchannel.ts
|
|
5540
5725
|
var DEFAULT_CUES = [
|
|
5541
5726
|
{ text: "mm-hmm" },
|
|
@@ -8154,6 +8339,9 @@ var createVoiceSession = (options) => {
|
|
|
8154
8339
|
};
|
|
8155
8340
|
const completeTurn = async (session, turn) => {
|
|
8156
8341
|
console.error(`[voice] completeTurn ENTER session=${options.id} turn=${turn.id} textLen=${turn.text?.length ?? 0}`);
|
|
8342
|
+
if (options.normalizeNumbers && turn.text) {
|
|
8343
|
+
turn.text = normalizeSpokenNumbers(turn.text);
|
|
8344
|
+
}
|
|
8157
8345
|
kickStuckCloseWatchdog();
|
|
8158
8346
|
kickIdleRepromptWatchdog(true);
|
|
8159
8347
|
const liveOpsControl = await options.liveOps?.getControl(options.id);
|
|
@@ -14396,6 +14584,16 @@ var createTwilioMediaStreamBridge = (socket, options) => {
|
|
|
14396
14584
|
}
|
|
14397
14585
|
return normalizePhraseHints(options.phraseHints);
|
|
14398
14586
|
};
|
|
14587
|
+
const resolveLanguageStrategy = async () => {
|
|
14588
|
+
if (typeof options.languageStrategy === "function") {
|
|
14589
|
+
return await options.languageStrategy({
|
|
14590
|
+
context: options.context,
|
|
14591
|
+
scenarioId: bridgeState.scenarioId ?? undefined,
|
|
14592
|
+
sessionId: bridgeState.sessionId ?? ""
|
|
14593
|
+
}) ?? undefined;
|
|
14594
|
+
}
|
|
14595
|
+
return options.languageStrategy;
|
|
14596
|
+
};
|
|
14399
14597
|
const ensureSession = async () => {
|
|
14400
14598
|
if (sessionHandle) {
|
|
14401
14599
|
return sessionHandle;
|
|
@@ -14403,6 +14601,7 @@ var createTwilioMediaStreamBridge = (socket, options) => {
|
|
|
14403
14601
|
bridgeState.sessionId ??= `phone-${Date.now().toString(36)}`;
|
|
14404
14602
|
const lexicon = await resolveLexicon();
|
|
14405
14603
|
const phraseHints = await resolvePhraseHints();
|
|
14604
|
+
const languageStrategy = await resolveLanguageStrategy();
|
|
14406
14605
|
const normalizedOnTurn = normalizeOnTurn(options.onTurn);
|
|
14407
14606
|
const route = {
|
|
14408
14607
|
correctTurn: options.correctTurn,
|
|
@@ -14434,13 +14633,14 @@ var createTwilioMediaStreamBridge = (socket, options) => {
|
|
|
14434
14633
|
const voiceSocket = createTwilioSocketAdapter(socket, () => bridgeState);
|
|
14435
14634
|
sessionHandle = createVoiceSession({
|
|
14436
14635
|
audioConditioning,
|
|
14636
|
+
normalizeNumbers: options.normalizeNumbers,
|
|
14437
14637
|
context: options.context,
|
|
14438
14638
|
costAccountant: options.costAccountant,
|
|
14439
14639
|
costTelemetry: options.costTelemetry,
|
|
14440
14640
|
costTelephony: options.costTelephony,
|
|
14441
14641
|
greeting: options.greeting,
|
|
14442
14642
|
id: bridgeState.sessionId,
|
|
14443
|
-
languageStrategy
|
|
14643
|
+
languageStrategy,
|
|
14444
14644
|
lexicon,
|
|
14445
14645
|
logger,
|
|
14446
14646
|
phraseHints,
|