@absolutejs/voice 0.0.22-beta.624 → 0.0.22-beta.626
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 +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +198 -2
- package/dist/testing/index.js +192 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const normalizeSpokenNumbers: (input: string) => string;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -861,6 +861,7 @@ export type VoicePluginConfig<TContext = unknown, TSession extends VoiceSessionR
|
|
|
861
861
|
defaultSilentTurnAck?: string;
|
|
862
862
|
routeOnTurnTimeoutMs?: number;
|
|
863
863
|
audioConditioning?: VoiceAudioConditioningConfig;
|
|
864
|
+
normalizeNumbers?: boolean;
|
|
864
865
|
noiseSuppressor?: VoiceNoiseSuppressor;
|
|
865
866
|
noiseSuppressorFormat?: AudioFormat;
|
|
866
867
|
logger?: VoiceLogger;
|
|
@@ -1131,6 +1132,7 @@ export type CreateVoiceSessionOptions<TContext = unknown, TSession extends Voice
|
|
|
1131
1132
|
sttLifecycle: VoiceSTTLifecycle;
|
|
1132
1133
|
turnDetection: VoiceResolvedTurnDetectionConfig;
|
|
1133
1134
|
audioConditioning?: VoiceResolvedAudioConditioningConfig;
|
|
1135
|
+
normalizeNumbers?: boolean;
|
|
1134
1136
|
noiseSuppressor?: VoiceNoiseSuppressor;
|
|
1135
1137
|
noiseSuppressorFormat?: AudioFormat;
|
|
1136
1138
|
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);
|
|
@@ -25468,6 +25656,7 @@ var createTwilioMediaStreamBridge = (socket, options) => {
|
|
|
25468
25656
|
const voiceSocket = createTwilioSocketAdapter(socket, () => bridgeState);
|
|
25469
25657
|
sessionHandle = createVoiceSession({
|
|
25470
25658
|
audioConditioning,
|
|
25659
|
+
normalizeNumbers: options.normalizeNumbers,
|
|
25471
25660
|
context: options.context,
|
|
25472
25661
|
costAccountant: options.costAccountant,
|
|
25473
25662
|
costTelemetry: options.costTelemetry,
|
|
@@ -25487,6 +25676,9 @@ var createTwilioMediaStreamBridge = (socket, options) => {
|
|
|
25487
25676
|
stt: options.stt,
|
|
25488
25677
|
sttFallback: resolveSTTFallbackConfig(options.sttFallback),
|
|
25489
25678
|
sttLifecycle: options.sttLifecycle ?? runtimePreset.sttLifecycle,
|
|
25679
|
+
sttRecoveryLine: options.sttRecoveryLine,
|
|
25680
|
+
stuckCallClose: options.stuckCallClose,
|
|
25681
|
+
idleReprompt: options.idleReprompt,
|
|
25490
25682
|
...options.semanticTurnDetector ? { semanticTurnDetector: options.semanticTurnDetector } : {},
|
|
25491
25683
|
...options.bargeInDetector ? { bargeInDetector: options.bargeInDetector } : {},
|
|
25492
25684
|
...options.fillerPhrases ? { fillerPhrases: options.fillerPhrases } : {},
|
|
@@ -39583,6 +39775,7 @@ var resolveSessionOptions = (config) => {
|
|
|
39583
39775
|
const preset = resolveVoiceRuntimePreset(config.preset);
|
|
39584
39776
|
return {
|
|
39585
39777
|
audioConditioning: config.audioConditioning !== undefined ? resolveAudioConditioningConfig(config.audioConditioning) : preset.audioConditioning,
|
|
39778
|
+
normalizeNumbers: config.normalizeNumbers,
|
|
39586
39779
|
noiseSuppressor: config.noiseSuppressor,
|
|
39587
39780
|
noiseSuppressorFormat: config.noiseSuppressorFormat,
|
|
39588
39781
|
costTelemetry: config.costTelemetry,
|
|
@@ -39796,6 +39989,7 @@ var voice = (config) => {
|
|
|
39796
39989
|
});
|
|
39797
39990
|
return createVoiceSession({
|
|
39798
39991
|
audioConditioning: sessionOptions.audioConditioning,
|
|
39992
|
+
normalizeNumbers: sessionOptions.normalizeNumbers,
|
|
39799
39993
|
noiseSuppressor: sessionOptions.noiseSuppressor,
|
|
39800
39994
|
noiseSuppressorFormat: sessionOptions.noiseSuppressorFormat,
|
|
39801
39995
|
context,
|
|
@@ -39804,6 +39998,7 @@ var voice = (config) => {
|
|
|
39804
39998
|
resumeGreeting: config.resumeGreeting,
|
|
39805
39999
|
sttRecoveryLine: config.sttRecoveryLine,
|
|
39806
40000
|
stuckCallClose: config.stuckCallClose,
|
|
40001
|
+
idleReprompt: config.idleReprompt,
|
|
39807
40002
|
handoff: config.handoff,
|
|
39808
40003
|
languageStrategy: config.languageStrategy,
|
|
39809
40004
|
lexicon,
|
|
@@ -50969,7 +51164,7 @@ var DEFAULT_VOICE_CAMPAIGN_TEMPLATE_FILTERS = {
|
|
|
50969
51164
|
ssml: (value) => escapeSpeech(String(value ?? "")),
|
|
50970
51165
|
upper: (value) => String(value ?? "").toUpperCase()
|
|
50971
51166
|
};
|
|
50972
|
-
var
|
|
51167
|
+
var renderValue2 = (value, filters, filterChain) => {
|
|
50973
51168
|
let cursor = value;
|
|
50974
51169
|
for (const segment of filterChain) {
|
|
50975
51170
|
const parts2 = segment.split(":");
|
|
@@ -51014,7 +51209,7 @@ var resolveVoiceCampaignTemplate = (template, options) => {
|
|
|
51014
51209
|
}
|
|
51015
51210
|
return options.fallback ?? "";
|
|
51016
51211
|
}
|
|
51017
|
-
return
|
|
51212
|
+
return renderValue2(value, filters, segments.slice(1));
|
|
51018
51213
|
});
|
|
51019
51214
|
return { missingVariables: Array.from(missing), output };
|
|
51020
51215
|
};
|
|
@@ -53647,6 +53842,7 @@ export {
|
|
|
53647
53842
|
parseVoiceSessionSnapshot,
|
|
53648
53843
|
parseVoiceAIScorecardResponse,
|
|
53649
53844
|
normalizeVoiceProofTrendReport,
|
|
53845
|
+
normalizeSpokenNumbers,
|
|
53650
53846
|
normalizePhoneNumber,
|
|
53651
53847
|
muteVoiceMonitorIssue,
|
|
53652
53848
|
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);
|
|
@@ -14434,6 +14622,7 @@ var createTwilioMediaStreamBridge = (socket, options) => {
|
|
|
14434
14622
|
const voiceSocket = createTwilioSocketAdapter(socket, () => bridgeState);
|
|
14435
14623
|
sessionHandle = createVoiceSession({
|
|
14436
14624
|
audioConditioning,
|
|
14625
|
+
normalizeNumbers: options.normalizeNumbers,
|
|
14437
14626
|
context: options.context,
|
|
14438
14627
|
costAccountant: options.costAccountant,
|
|
14439
14628
|
costTelemetry: options.costTelemetry,
|
|
@@ -14453,6 +14642,9 @@ var createTwilioMediaStreamBridge = (socket, options) => {
|
|
|
14453
14642
|
stt: options.stt,
|
|
14454
14643
|
sttFallback: resolveSTTFallbackConfig(options.sttFallback),
|
|
14455
14644
|
sttLifecycle: options.sttLifecycle ?? runtimePreset.sttLifecycle,
|
|
14645
|
+
sttRecoveryLine: options.sttRecoveryLine,
|
|
14646
|
+
stuckCallClose: options.stuckCallClose,
|
|
14647
|
+
idleReprompt: options.idleReprompt,
|
|
14456
14648
|
...options.semanticTurnDetector ? { semanticTurnDetector: options.semanticTurnDetector } : {},
|
|
14457
14649
|
...options.bargeInDetector ? { bargeInDetector: options.bargeInDetector } : {},
|
|
14458
14650
|
...options.fillerPhrases ? { fillerPhrases: options.fillerPhrases } : {},
|