@absolutejs/voice 0.0.22-beta.625 → 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 +194 -2
- package/dist/testing/index.js +189 -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,
|
|
@@ -39586,6 +39775,7 @@ var resolveSessionOptions = (config) => {
|
|
|
39586
39775
|
const preset = resolveVoiceRuntimePreset(config.preset);
|
|
39587
39776
|
return {
|
|
39588
39777
|
audioConditioning: config.audioConditioning !== undefined ? resolveAudioConditioningConfig(config.audioConditioning) : preset.audioConditioning,
|
|
39778
|
+
normalizeNumbers: config.normalizeNumbers,
|
|
39589
39779
|
noiseSuppressor: config.noiseSuppressor,
|
|
39590
39780
|
noiseSuppressorFormat: config.noiseSuppressorFormat,
|
|
39591
39781
|
costTelemetry: config.costTelemetry,
|
|
@@ -39799,6 +39989,7 @@ var voice = (config) => {
|
|
|
39799
39989
|
});
|
|
39800
39990
|
return createVoiceSession({
|
|
39801
39991
|
audioConditioning: sessionOptions.audioConditioning,
|
|
39992
|
+
normalizeNumbers: sessionOptions.normalizeNumbers,
|
|
39802
39993
|
noiseSuppressor: sessionOptions.noiseSuppressor,
|
|
39803
39994
|
noiseSuppressorFormat: sessionOptions.noiseSuppressorFormat,
|
|
39804
39995
|
context,
|
|
@@ -50973,7 +51164,7 @@ var DEFAULT_VOICE_CAMPAIGN_TEMPLATE_FILTERS = {
|
|
|
50973
51164
|
ssml: (value) => escapeSpeech(String(value ?? "")),
|
|
50974
51165
|
upper: (value) => String(value ?? "").toUpperCase()
|
|
50975
51166
|
};
|
|
50976
|
-
var
|
|
51167
|
+
var renderValue2 = (value, filters, filterChain) => {
|
|
50977
51168
|
let cursor = value;
|
|
50978
51169
|
for (const segment of filterChain) {
|
|
50979
51170
|
const parts2 = segment.split(":");
|
|
@@ -51018,7 +51209,7 @@ var resolveVoiceCampaignTemplate = (template, options) => {
|
|
|
51018
51209
|
}
|
|
51019
51210
|
return options.fallback ?? "";
|
|
51020
51211
|
}
|
|
51021
|
-
return
|
|
51212
|
+
return renderValue2(value, filters, segments.slice(1));
|
|
51022
51213
|
});
|
|
51023
51214
|
return { missingVariables: Array.from(missing), output };
|
|
51024
51215
|
};
|
|
@@ -53651,6 +53842,7 @@ export {
|
|
|
53651
53842
|
parseVoiceSessionSnapshot,
|
|
53652
53843
|
parseVoiceAIScorecardResponse,
|
|
53653
53844
|
normalizeVoiceProofTrendReport,
|
|
53845
|
+
normalizeSpokenNumbers,
|
|
53654
53846
|
normalizePhoneNumber,
|
|
53655
53847
|
muteVoiceMonitorIssue,
|
|
53656
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,
|