@pipedog/core 0.1.0 → 0.2.0
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/audio/resamplers/engine.d.ts +60 -0
- package/dist/audio/resamplers/engine.js +89 -0
- package/dist/audio/resamplers/sinc_engine.d.ts +10 -0
- package/dist/audio/resamplers/sinc_engine.js +235 -0
- package/dist/audio/resamplers/soxr_resampler.d.ts +10 -6
- package/dist/audio/resamplers/soxr_resampler.js +17 -7
- package/dist/audio/resamplers/soxr_stream_resampler.d.ts +9 -6
- package/dist/audio/resamplers/soxr_stream_resampler.js +24 -11
- package/dist/audio/turn/smart_turn/local_smart_turn_v3.js +16 -8
- package/dist/audio/vad/vad_analyzer.d.ts +11 -3
- package/dist/audio/vad/vad_analyzer.js +72 -34
- package/dist/pipeline/worker.d.ts +7 -0
- package/dist/pipeline/worker.js +40 -23
- package/dist/utils/file_storage.d.ts +37 -21
- package/dist/utils/file_storage.js +87 -9
- package/dist/utils/frame_queue.js +2 -2
- package/dist/utils/image.d.ts +0 -1
- package/dist/utils/image.js +37 -4
- package/dist/utils/jpeg_encoder.d.ts +14 -0
- package/dist/utils/jpeg_encoder.js +315 -0
- package/dist/utils/text/transforms/acronyms.d.ts +21 -0
- package/dist/utils/text/transforms/acronyms.js +31 -0
- package/dist/utils/text/transforms/currency.d.ts +16 -0
- package/dist/utils/text/transforms/currency.js +56 -0
- package/dist/utils/text/transforms/dates.d.ts +18 -0
- package/dist/utils/text/transforms/dates.js +102 -0
- package/dist/utils/text/transforms/email.d.ts +17 -0
- package/dist/utils/text/transforms/email.js +35 -0
- package/dist/utils/text/transforms/index.d.ts +37 -0
- package/dist/utils/text/transforms/index.js +42 -0
- package/dist/utils/text/transforms/numbers.d.ts +25 -0
- package/dist/utils/text/transforms/numbers.js +59 -0
- package/dist/utils/text/transforms/percentages.d.ts +16 -0
- package/dist/utils/text/transforms/percentages.js +32 -0
- package/dist/utils/text/transforms/phone.d.ts +21 -0
- package/dist/utils/text/transforms/phone.js +39 -0
- package/dist/utils/text/transforms/pronunciations.js +2 -1
- package/dist/utils/text/transforms/replacements.d.ts +39 -0
- package/dist/utils/text/transforms/replacements.js +46 -0
- package/dist/utils/text/transforms/strip_markdown.d.ts +25 -0
- package/dist/utils/text/transforms/strip_markdown.js +60 -0
- package/dist/utils/text/transforms/units.d.ts +22 -0
- package/dist/utils/text/transforms/units.js +81 -0
- package/dist/utils/text/transforms/voice_formatter.d.ts +86 -0
- package/dist/utils/text/transforms/voice_formatter.js +98 -0
- package/dist/utils/text/unicode_digits.d.ts +3 -0
- package/dist/utils/text/unicode_digits.js +34 -0
- package/dist/utils/tracing/service_decorators.js +2 -2
- package/dist/utils/tracing/setup.d.ts +18 -6
- package/dist/utils/tracing/setup.js +63 -13
- package/dist/utils/tracing/turn_trace_observer.d.ts +5 -0
- package/dist/utils/tracing/turn_trace_observer.js +18 -4
- package/dist/utils/yaml.d.ts +151 -36
- package/dist/utils/yaml.js +527 -12
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workers/proxy/index.d.ts +3 -0
- package/dist/workers/proxy/index.js +9 -0
- package/dist/workers/proxy/websocket/client.d.ts +110 -0
- package/dist/workers/proxy/websocket/client.js +196 -0
- package/dist/workers/proxy/websocket/index.d.ts +3 -0
- package/dist/workers/proxy/websocket/index.js +10 -0
- package/package.json +18 -3
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026, Daily
|
|
2
|
+
// Copyright (c) 2026, pipedog contributors
|
|
3
|
+
// SPDX-License-Identifier: BSD-2-Clause
|
|
4
|
+
//
|
|
5
|
+
// Ported from pipecat: src/pipecat/utils/text/transforms/__init__.py
|
|
6
|
+
/**
|
|
7
|
+
* Built-in text transformers for TTS voice formatting.
|
|
8
|
+
*
|
|
9
|
+
* Transformers are async callables with signature:
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* async function transform(text: string, textType: string): Promise<string>
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* They are registered with a TTS service via `textTransforms`:
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* const tts = new CartesiaTTSService({
|
|
19
|
+
* textTransforms: [["*", stripMarkdown], ["*", expandCurrency]],
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* Or use `VoiceFormatter` for a single configurable bundle:
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* const tts = new CartesiaTTSService({
|
|
27
|
+
* textTransforms: [["*", new VoiceFormatter({ expandCurrency: true })]],
|
|
28
|
+
* });
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export { normalizeAcronyms } from "./acronyms.js";
|
|
32
|
+
export { expandCurrency } from "./currency.js";
|
|
33
|
+
export { normalizeDates } from "./dates.js";
|
|
34
|
+
export { emailToSpeech } from "./email.js";
|
|
35
|
+
export { expandNumbers } from "./numbers.js";
|
|
36
|
+
export { expandPercentages } from "./percentages.js";
|
|
37
|
+
export { expandPhoneNumbers } from "./phone.js";
|
|
38
|
+
export { replaceText } from "./replacements.js";
|
|
39
|
+
export { stripMarkdown } from "./strip_markdown.js";
|
|
40
|
+
export { expandUnits } from "./units.js";
|
|
41
|
+
export { VoiceFormatter } from "./voice_formatter.js";
|
|
42
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { TextType } from "../../../frames/frames.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Return a transform that expands numbers to their spoken form.
|
|
4
|
+
*
|
|
5
|
+
* When `digitCutoff` is set, numbers above it are read digit-by-digit
|
|
6
|
+
* (e.g. `"2026"` → `"2 0 2 6"`). Numbers at or below the cutoff are
|
|
7
|
+
* expanded as quantities (e.g. `"42"` → `"forty two"`). Pass `null`
|
|
8
|
+
* to expand all numbers as words regardless of magnitude.
|
|
9
|
+
*
|
|
10
|
+
* Decimals are read as the whole part followed by their fractional digits, one at a
|
|
11
|
+
* time, so every written digit is spoken (`"1.0"` → `"one point zero"`).
|
|
12
|
+
*
|
|
13
|
+
* @param digitCutoff Numbers larger than this value are read digit-by-digit.
|
|
14
|
+
* `null` disables the cutoff so every number is expanded as a word.
|
|
15
|
+
* @returns An async transform callable compatible with `textTransforms`.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* const transform = expandNumbers(2025);
|
|
20
|
+
* const result = await transform("Room 42 has 1234 seats and opens in 2026", "*");
|
|
21
|
+
* // "Room forty two has one thousand two hundred thirty four seats and opens in 2 0 2 6"
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function expandNumbers(digitCutoff?: number | bigint | null): (text: string, textType: string | TextType) => Promise<string>;
|
|
25
|
+
//# sourceMappingURL=numbers.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026, Daily
|
|
2
|
+
// Copyright (c) 2026, pipedog contributors
|
|
3
|
+
// SPDX-License-Identifier: BSD-2-Clause
|
|
4
|
+
//
|
|
5
|
+
// Ported from pipecat: src/pipecat/utils/text/transforms/numbers.py
|
|
6
|
+
/** Expand numeric expressions into spoken form for TTS. */
|
|
7
|
+
import { num2words } from "num2words-ts";
|
|
8
|
+
import { toAsciiDigits } from "../unicode_digits.js";
|
|
9
|
+
// Matches integers and decimals, with optional thousand separators.
|
|
10
|
+
// Python's `\b` (next to a digit here) is Unicode-aware; the lookarounds
|
|
11
|
+
// rebuild it with Unicode `\w` (letters, digits and underscore).
|
|
12
|
+
// `\p{Nd}` is Python's Unicode `\d`; toAsciiDigits() lets BigInt/Number parse what int()/float() accept.
|
|
13
|
+
const NUMBER_RE = /(?<![\p{L}\p{N}_])(\p{Nd}{1,3}(?:,\p{Nd}{3})*|\p{Nd}+)(?:\.(\p{Nd}+))?(?![\p{L}\p{N}_])/gu;
|
|
14
|
+
/**
|
|
15
|
+
* Return a transform that expands numbers to their spoken form.
|
|
16
|
+
*
|
|
17
|
+
* When `digitCutoff` is set, numbers above it are read digit-by-digit
|
|
18
|
+
* (e.g. `"2026"` → `"2 0 2 6"`). Numbers at or below the cutoff are
|
|
19
|
+
* expanded as quantities (e.g. `"42"` → `"forty two"`). Pass `null`
|
|
20
|
+
* to expand all numbers as words regardless of magnitude.
|
|
21
|
+
*
|
|
22
|
+
* Decimals are read as the whole part followed by their fractional digits, one at a
|
|
23
|
+
* time, so every written digit is spoken (`"1.0"` → `"one point zero"`).
|
|
24
|
+
*
|
|
25
|
+
* @param digitCutoff Numbers larger than this value are read digit-by-digit.
|
|
26
|
+
* `null` disables the cutoff so every number is expanded as a word.
|
|
27
|
+
* @returns An async transform callable compatible with `textTransforms`.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* const transform = expandNumbers(2025);
|
|
32
|
+
* const result = await transform("Room 42 has 1234 seats and opens in 2026", "*");
|
|
33
|
+
* // "Room forty two has one thousand two hundred thirty four seats and opens in 2 0 2 6"
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function expandNumbers(digitCutoff = 2025) {
|
|
37
|
+
const numToWords = (wholeMatch, fracStr) => {
|
|
38
|
+
const wholeStr = wholeMatch.replaceAll(",", "");
|
|
39
|
+
const whole = BigInt(toAsciiDigits(wholeStr));
|
|
40
|
+
if (digitCutoff !== null && whole > digitCutoff) {
|
|
41
|
+
let result = [...wholeStr].join(" ");
|
|
42
|
+
if (fracStr) {
|
|
43
|
+
result += ` point ${[...fracStr].join(" ")}`;
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
if (fracStr) {
|
|
48
|
+
// Reading the fraction one digit at a time keeps trailing zeros audible,
|
|
49
|
+
// which also keeps a decimal agreeing with the plural unit expandUnits
|
|
50
|
+
// picks for it ("1.0 kilometers").
|
|
51
|
+
return `${num2words(whole, { lang: "en" })} point ${[...fracStr]
|
|
52
|
+
.map((digit) => num2words(Number(toAsciiDigits(digit)), { lang: "en" }))
|
|
53
|
+
.join(" ")}`;
|
|
54
|
+
}
|
|
55
|
+
return num2words(whole, { lang: "en" });
|
|
56
|
+
};
|
|
57
|
+
return async (text, _textType) => text.replace(NUMBER_RE, (_match, whole, frac) => numToWords(whole, frac));
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=numbers.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { TextType } from "../../../frames/frames.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Expand percentage expressions to their spoken form.
|
|
4
|
+
*
|
|
5
|
+
* @param text Input text possibly containing percentage expressions.
|
|
6
|
+
* @param _textType Text type of the text frame (unused).
|
|
7
|
+
* @returns Text with percentages replaced by spoken equivalents.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const result = await expandPercentages("50% off today", "*");
|
|
12
|
+
* // "fifty percent off today"
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare function expandPercentages(text: string, _textType: string | TextType): Promise<string>;
|
|
16
|
+
//# sourceMappingURL=percentages.d.ts.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026, Daily
|
|
2
|
+
// Copyright (c) 2026, pipedog contributors
|
|
3
|
+
// SPDX-License-Identifier: BSD-2-Clause
|
|
4
|
+
//
|
|
5
|
+
// Ported from pipecat: src/pipecat/utils/text/transforms/percentages.py
|
|
6
|
+
/** Expand percentage expressions into spoken form for TTS. */
|
|
7
|
+
import { asFloat, num2words } from "num2words-ts";
|
|
8
|
+
import { toAsciiDigits } from "../unicode_digits.js";
|
|
9
|
+
// `\p{Nd}` is Python's Unicode `\d`; toAsciiDigits() lets BigInt/Number parse what int()/float() accept.
|
|
10
|
+
const PERCENT_RE = /(\p{Nd}+(?:\.\p{Nd}+)?)\s*%/gu;
|
|
11
|
+
function percentToWords(value) {
|
|
12
|
+
// Upstream converts with `float(value)`, so "50" reads as the float 50.0.
|
|
13
|
+
const numberStr = num2words(asFloat(Number.parseFloat(toAsciiDigits(value))), { lang: "en" });
|
|
14
|
+
return `${numberStr} percent`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Expand percentage expressions to their spoken form.
|
|
18
|
+
*
|
|
19
|
+
* @param text Input text possibly containing percentage expressions.
|
|
20
|
+
* @param _textType Text type of the text frame (unused).
|
|
21
|
+
* @returns Text with percentages replaced by spoken equivalents.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* const result = await expandPercentages("50% off today", "*");
|
|
26
|
+
* // "fifty percent off today"
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export async function expandPercentages(text, _textType) {
|
|
30
|
+
return text.replace(PERCENT_RE, (_match, value) => percentToWords(value));
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=percentages.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Expand phone numbers into space-separated digit sequences for TTS. */
|
|
2
|
+
import type { TextType } from "../../../frames/frames.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Space out phone number digits so TTS reads them individually.
|
|
5
|
+
*
|
|
6
|
+
* This transformer is alphanumeric-preserving: digits are kept, only separators
|
|
7
|
+
* change to spaces, so the `WordCompletionTracker` requires no segment-map
|
|
8
|
+
* overhead.
|
|
9
|
+
*
|
|
10
|
+
* @param text Input text possibly containing phone numbers.
|
|
11
|
+
* @param _textType Text type of the text frame (unused).
|
|
12
|
+
* @returns Text with phone numbers replaced by space-separated digit sequences.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* const result = await expandPhoneNumbers("Call 123-456-7890 now", "*");
|
|
17
|
+
* // "Call 1 2 3 4 5 6 7 8 9 0 now"
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare function expandPhoneNumbers(text: string, _textType: string | TextType): Promise<string>;
|
|
21
|
+
//# sourceMappingURL=phone.d.ts.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026, Daily
|
|
2
|
+
// Copyright (c) 2026, pipedog contributors
|
|
3
|
+
// SPDX-License-Identifier: BSD-2-Clause
|
|
4
|
+
//
|
|
5
|
+
// Ported from pipecat: src/pipecat/utils/text/transforms/phone.py
|
|
6
|
+
// Matches common phone formats: (123) 456-7890, 123-456-7890, 123.456.7890,
|
|
7
|
+
// +1 800 555 1234, etc.
|
|
8
|
+
const PHONE_RE = new RegExp("(?<!\\p{Nd})" + // not preceded by a digit (no partial match inside a longer number)
|
|
9
|
+
"(\\+?1[\\s.\\-]?)?" + // optional country code
|
|
10
|
+
"(\\(?\\p{Nd}{3}\\)?[\\s.\\-]?)" + // area code
|
|
11
|
+
"(\\p{Nd}{3}[\\s.\\-]?)" + // exchange
|
|
12
|
+
"(\\p{Nd}{4})" + // subscriber
|
|
13
|
+
"(?!\\p{Nd})", // not followed by a digit
|
|
14
|
+
"gu");
|
|
15
|
+
function spaceDigits(match) {
|
|
16
|
+
const digits = match.replace(/\P{Nd}/gu, "");
|
|
17
|
+
return [...digits].join(" ");
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Space out phone number digits so TTS reads them individually.
|
|
21
|
+
*
|
|
22
|
+
* This transformer is alphanumeric-preserving: digits are kept, only separators
|
|
23
|
+
* change to spaces, so the `WordCompletionTracker` requires no segment-map
|
|
24
|
+
* overhead.
|
|
25
|
+
*
|
|
26
|
+
* @param text Input text possibly containing phone numbers.
|
|
27
|
+
* @param _textType Text type of the text frame (unused).
|
|
28
|
+
* @returns Text with phone numbers replaced by space-separated digit sequences.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* const result = await expandPhoneNumbers("Call 123-456-7890 now", "*");
|
|
33
|
+
* // "Call 1 2 3 4 5 6 7 8 9 0 now"
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export async function expandPhoneNumbers(text, _textType) {
|
|
37
|
+
return text.replace(PHONE_RE, (match) => spaceDigits(match));
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=phone.js.map
|
|
@@ -93,7 +93,8 @@ export function pronunciationTransform(pronunciations, formatPronunciation, opti
|
|
|
93
93
|
}
|
|
94
94
|
let pattern = null;
|
|
95
95
|
if (phonemes.size > 0) {
|
|
96
|
-
|
|
96
|
+
// Python's `len` counts code points, not UTF-16 units.
|
|
97
|
+
const words = [...phonemes.keys()].sort((a, b) => [...b].length - [...a].length);
|
|
97
98
|
// Python's Unicode `\w` is letters, digits and underscore.
|
|
98
99
|
pattern = new RegExp(`(?<![\\p{L}\\p{N}_-])(?:${words.map(escapeRegex).join("|")})(?![\\p{L}\\p{N}_-])`, "giu");
|
|
99
100
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-defined text replacements for TTS preprocessing.
|
|
3
|
+
*
|
|
4
|
+
* Porting notes: patterns are JavaScript regular expressions (a `RegExp`, or a
|
|
5
|
+
* string compiled with `new RegExp(pattern, "g")`), and replacement strings use
|
|
6
|
+
* JavaScript's `String.prototype.replace` syntax (`$1`, `$<name>`) rather than
|
|
7
|
+
* Python's (`\1`, `\g<name>`). Every match is replaced, as Python's `re.sub`
|
|
8
|
+
* does, whether or not a `RegExp` carries the `g` flag.
|
|
9
|
+
*/
|
|
10
|
+
import type { TextType } from "../../../frames/frames.ts";
|
|
11
|
+
/** A find-and-replace rule: `(regexPattern, replacementString)`. */
|
|
12
|
+
export type TextReplacement = readonly [pattern: string | RegExp, replacement: string];
|
|
13
|
+
/**
|
|
14
|
+
* Return a transform that applies a list of find-and-replace rules.
|
|
15
|
+
*
|
|
16
|
+
* Each rule is a `(pattern, replacement)` tuple. Patterns are treated as
|
|
17
|
+
* regular expressions; escape special characters for literal string matching.
|
|
18
|
+
*
|
|
19
|
+
* Rules are applied in the order provided. Whether the resulting transform is
|
|
20
|
+
* alphanumeric-preserving depends on the replacements supplied.
|
|
21
|
+
*
|
|
22
|
+
* Patterns are compiled at construction time so invalid regex patterns raise
|
|
23
|
+
* `SyntaxError` immediately rather than during live TTS processing.
|
|
24
|
+
*
|
|
25
|
+
* @param replacements Ordered list of `(regexPattern, replacementString)` pairs.
|
|
26
|
+
* @returns An async transform callable compatible with `textTransforms`.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* const transform = replaceText([
|
|
31
|
+
* [/\bDr\./, "Doctor"],
|
|
32
|
+
* [/\bSt\./, "Street"],
|
|
33
|
+
* [/\bvs\b/, "versus"],
|
|
34
|
+
* ]);
|
|
35
|
+
* const tts = new CartesiaTTSService({ textTransforms: [["*", transform]] });
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function replaceText(replacements: readonly TextReplacement[]): (text: string, textType: string | TextType) => Promise<string>;
|
|
39
|
+
//# sourceMappingURL=replacements.d.ts.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026, Daily
|
|
2
|
+
// Copyright (c) 2026, pipedog contributors
|
|
3
|
+
// SPDX-License-Identifier: BSD-2-Clause
|
|
4
|
+
//
|
|
5
|
+
// Ported from pipecat: src/pipecat/utils/text/transforms/replacements.py
|
|
6
|
+
function compile(pattern) {
|
|
7
|
+
if (typeof pattern === "string")
|
|
8
|
+
return new RegExp(pattern, "g");
|
|
9
|
+
return pattern.global ? new RegExp(pattern) : new RegExp(pattern, `${pattern.flags}g`);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Return a transform that applies a list of find-and-replace rules.
|
|
13
|
+
*
|
|
14
|
+
* Each rule is a `(pattern, replacement)` tuple. Patterns are treated as
|
|
15
|
+
* regular expressions; escape special characters for literal string matching.
|
|
16
|
+
*
|
|
17
|
+
* Rules are applied in the order provided. Whether the resulting transform is
|
|
18
|
+
* alphanumeric-preserving depends on the replacements supplied.
|
|
19
|
+
*
|
|
20
|
+
* Patterns are compiled at construction time so invalid regex patterns raise
|
|
21
|
+
* `SyntaxError` immediately rather than during live TTS processing.
|
|
22
|
+
*
|
|
23
|
+
* @param replacements Ordered list of `(regexPattern, replacementString)` pairs.
|
|
24
|
+
* @returns An async transform callable compatible with `textTransforms`.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* const transform = replaceText([
|
|
29
|
+
* [/\bDr\./, "Doctor"],
|
|
30
|
+
* [/\bSt\./, "Street"],
|
|
31
|
+
* [/\bvs\b/, "versus"],
|
|
32
|
+
* ]);
|
|
33
|
+
* const tts = new CartesiaTTSService({ textTransforms: [["*", transform]] });
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function replaceText(replacements) {
|
|
37
|
+
const compiled = replacements.map(([pattern, replacement]) => [compile(pattern), replacement]);
|
|
38
|
+
return async (text, _textType) => {
|
|
39
|
+
for (const [pattern, replacement] of compiled) {
|
|
40
|
+
pattern.lastIndex = 0;
|
|
41
|
+
text = text.replace(pattern, replacement);
|
|
42
|
+
}
|
|
43
|
+
return text;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=replacements.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Strip Markdown formatting symbols before TTS synthesis. */
|
|
2
|
+
import type { TextType } from "../../../frames/frames.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Remove Markdown formatting symbols that have no spoken equivalent.
|
|
5
|
+
*
|
|
6
|
+
* Strips bold/italic markers, backtick code spans, fenced code blocks, ATX
|
|
7
|
+
* headers, and blockquote markers. Does not modify link or image syntax since
|
|
8
|
+
* those may carry meaningful text (the link label is preserved verbatim).
|
|
9
|
+
*
|
|
10
|
+
* This transformer is alphanumeric-preserving: the normalized alnum sequence of
|
|
11
|
+
* the output is identical to the input, so the `WordCompletionTracker`
|
|
12
|
+
* requires no segment-map overhead.
|
|
13
|
+
*
|
|
14
|
+
* @param text Input text, potentially containing Markdown formatting.
|
|
15
|
+
* @param _textType Text type of the text frame (unused).
|
|
16
|
+
* @returns Text with Markdown formatting symbols removed.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* const result = await stripMarkdown("**Hello** and _world_", "*");
|
|
21
|
+
* // "Hello and world"
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function stripMarkdown(text: string, _textType: string | TextType): Promise<string>;
|
|
25
|
+
//# sourceMappingURL=strip_markdown.d.ts.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026, Daily
|
|
2
|
+
// Copyright (c) 2026, pipedog contributors
|
|
3
|
+
// SPDX-License-Identifier: BSD-2-Clause
|
|
4
|
+
//
|
|
5
|
+
// Ported from pipecat: src/pipecat/utils/text/transforms/strip_markdown.py
|
|
6
|
+
// Translation notes for the patterns below:
|
|
7
|
+
// - Python's `.` matches anything but `\n` (JS's also skips `\r`, U+2028 and
|
|
8
|
+
// U+2029), so it is written `[^\n]`.
|
|
9
|
+
// - Python's `re.MULTILINE` `^` / `$` only break lines at `\n` (JS's `m` flag
|
|
10
|
+
// also breaks at `\r`, U+2028 and U+2029), so they are written
|
|
11
|
+
// `(?<![^\n])` / `(?![^\n])`.
|
|
12
|
+
// - Python's `\b` is Unicode-aware; next to `_` (a word character) it becomes a
|
|
13
|
+
// lookaround on Unicode `\w` (letters, digits and underscore).
|
|
14
|
+
const W = "[\\p{L}\\p{N}_]";
|
|
15
|
+
const keepGroup = (_match, inner) => inner;
|
|
16
|
+
/**
|
|
17
|
+
* Remove Markdown formatting symbols that have no spoken equivalent.
|
|
18
|
+
*
|
|
19
|
+
* Strips bold/italic markers, backtick code spans, fenced code blocks, ATX
|
|
20
|
+
* headers, and blockquote markers. Does not modify link or image syntax since
|
|
21
|
+
* those may carry meaningful text (the link label is preserved verbatim).
|
|
22
|
+
*
|
|
23
|
+
* This transformer is alphanumeric-preserving: the normalized alnum sequence of
|
|
24
|
+
* the output is identical to the input, so the `WordCompletionTracker`
|
|
25
|
+
* requires no segment-map overhead.
|
|
26
|
+
*
|
|
27
|
+
* @param text Input text, potentially containing Markdown formatting.
|
|
28
|
+
* @param _textType Text type of the text frame (unused).
|
|
29
|
+
* @returns Text with Markdown formatting symbols removed.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* const result = await stripMarkdown("**Hello** and _world_", "*");
|
|
34
|
+
* // "Hello and world"
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export async function stripMarkdown(text, _textType) {
|
|
38
|
+
// Fenced code blocks (``` or ~~~)
|
|
39
|
+
text = text.replace(/```[\s\S]*?```/g, "");
|
|
40
|
+
text = text.replace(/~~~[\s\S]*?~~~/g, "");
|
|
41
|
+
// Inline code
|
|
42
|
+
text = text.replace(/`([^`]+)`/g, keepGroup);
|
|
43
|
+
// Bold+italic ***text*** or ___text___
|
|
44
|
+
text = text.replace(/\*{3}([^\n]+?)\*{3}/g, keepGroup);
|
|
45
|
+
text = text.replace(/_{3}([^\n]+?)_{3}/g, keepGroup);
|
|
46
|
+
// Bold **text** or __text__
|
|
47
|
+
text = text.replace(/\*{2}([^\n]+?)\*{2}/g, keepGroup);
|
|
48
|
+
text = text.replace(/_{2}([^\n]+?)_{2}/g, keepGroup);
|
|
49
|
+
// Italic *text* or _text_
|
|
50
|
+
text = text.replace(/\*([^\n]+?)\*/g, keepGroup);
|
|
51
|
+
text = text.replace(new RegExp(`(?<!${W})_([^\\n]+?)_(?!${W})`, "gu"), keepGroup);
|
|
52
|
+
// ATX headers (# ## ### etc.)
|
|
53
|
+
text = text.replace(/(?<![^\n])#{1,6}\s+/g, "");
|
|
54
|
+
// Blockquotes
|
|
55
|
+
text = text.replace(/(?<![^\n])>\s?/g, "");
|
|
56
|
+
// Horizontal rules
|
|
57
|
+
text = text.replace(/(?<![^\n])\s*[-*_]{3,}\s*(?![^\n])/g, "");
|
|
58
|
+
return text;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=strip_markdown.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Expand unit abbreviations into their spoken form for TTS. */
|
|
2
|
+
import type { TextType } from "../../../frames/frames.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Expand unit abbreviations to their full spoken form.
|
|
5
|
+
*
|
|
6
|
+
* A quantity of exactly one takes the singular form of the unit.
|
|
7
|
+
*
|
|
8
|
+
* @param text Input text possibly containing unit expressions.
|
|
9
|
+
* @param _textType Text type of the text frame (unused).
|
|
10
|
+
* @returns Text with unit abbreviations replaced by spoken equivalents.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const result = await expandUnits("Run 5km at 100kph", "*");
|
|
15
|
+
* // "Run 5 kilometers at 100 kilometers per hour"
|
|
16
|
+
*
|
|
17
|
+
* const result2 = await expandUnits("Only 1km left", "*");
|
|
18
|
+
* // "Only 1 kilometer left"
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare function expandUnits(text: string, _textType: string | TextType): Promise<string>;
|
|
22
|
+
//# sourceMappingURL=units.d.ts.map
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Copyright (c) 2024-2026, Daily
|
|
2
|
+
// Copyright (c) 2026, pipedog contributors
|
|
3
|
+
// SPDX-License-Identifier: BSD-2-Clause
|
|
4
|
+
//
|
|
5
|
+
// Ported from pipecat: src/pipecat/utils/text/transforms/units.py
|
|
6
|
+
/** Maps unit abbreviation to (singular, plural) spoken forms. */
|
|
7
|
+
const UNIT_MAP = {
|
|
8
|
+
km: ["kilometer", "kilometers"],
|
|
9
|
+
m: ["meter", "meters"],
|
|
10
|
+
cm: ["centimeter", "centimeters"],
|
|
11
|
+
mm: ["millimeter", "millimeters"],
|
|
12
|
+
mi: ["mile", "miles"],
|
|
13
|
+
ft: ["foot", "feet"],
|
|
14
|
+
in: ["inch", "inches"],
|
|
15
|
+
yd: ["yard", "yards"],
|
|
16
|
+
kg: ["kilogram", "kilograms"],
|
|
17
|
+
g: ["gram", "grams"],
|
|
18
|
+
mg: ["milligram", "milligrams"],
|
|
19
|
+
lb: ["pound", "pounds"],
|
|
20
|
+
oz: ["ounce", "ounces"],
|
|
21
|
+
l: ["liter", "liters"],
|
|
22
|
+
ml: ["milliliter", "milliliters"],
|
|
23
|
+
mph: ["mile per hour", "miles per hour"],
|
|
24
|
+
kph: ["kilometer per hour", "kilometers per hour"],
|
|
25
|
+
kmh: ["kilometer per hour", "kilometers per hour"],
|
|
26
|
+
gb: ["gigabyte", "gigabytes"],
|
|
27
|
+
mb: ["megabyte", "megabytes"],
|
|
28
|
+
kb: ["kilobyte", "kilobytes"],
|
|
29
|
+
tb: ["terabyte", "terabytes"],
|
|
30
|
+
hz: ["hertz", "hertz"],
|
|
31
|
+
khz: ["kilohertz", "kilohertz"],
|
|
32
|
+
mhz: ["megahertz", "megahertz"],
|
|
33
|
+
ghz: ["gigahertz", "gigahertz"],
|
|
34
|
+
};
|
|
35
|
+
// Single-letter units that are also common English words: only expand when
|
|
36
|
+
// they appear immediately after a digit with no intervening space, e.g. "5m"
|
|
37
|
+
// but not "1 m people" (where "m" is a word) or "1 in 5" (preposition).
|
|
38
|
+
const AMBIGUOUS_UNITS = new Set(["in", "m", "g", "l"]);
|
|
39
|
+
// A stable sort, like Python's `sorted`, so equal lengths keep insertion order.
|
|
40
|
+
const sortedUnambiguous = Object.keys(UNIT_MAP)
|
|
41
|
+
.filter((u) => !AMBIGUOUS_UNITS.has(u))
|
|
42
|
+
.sort((a, b) => b.length - a.length);
|
|
43
|
+
const sortedAmbiguous = [...AMBIGUOUS_UNITS].sort((a, b) => b.length - a.length);
|
|
44
|
+
// Python's `\b` (after a unit letter here) is Unicode-aware; the lookahead
|
|
45
|
+
// rebuilds it with Unicode `\w` (letters, digits and underscore). The unit
|
|
46
|
+
// names are plain letters, so they need no escaping.
|
|
47
|
+
const W = "[\\p{L}\\p{N}_]";
|
|
48
|
+
// Unambiguous units allow optional whitespace between the number and the unit.
|
|
49
|
+
const UNIT_RE = new RegExp(`(\\p{Nd}+(?:\\.\\p{Nd}+)?)\\s*(${sortedUnambiguous.join("|")})(?!${W})`, "giu");
|
|
50
|
+
// Ambiguous units require the unit to follow the digit with no space.
|
|
51
|
+
const AMBIGUOUS_UNIT_RE = new RegExp(`(\\p{Nd}+(?:\\.\\p{Nd}+)?)(${sortedAmbiguous.join("|")})(?!${W})`, "giu");
|
|
52
|
+
/**
|
|
53
|
+
* Expand unit abbreviations to their full spoken form.
|
|
54
|
+
*
|
|
55
|
+
* A quantity of exactly one takes the singular form of the unit.
|
|
56
|
+
*
|
|
57
|
+
* @param text Input text possibly containing unit expressions.
|
|
58
|
+
* @param _textType Text type of the text frame (unused).
|
|
59
|
+
* @returns Text with unit abbreviations replaced by spoken equivalents.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* const result = await expandUnits("Run 5km at 100kph", "*");
|
|
64
|
+
* // "Run 5 kilometers at 100 kilometers per hour"
|
|
65
|
+
*
|
|
66
|
+
* const result2 = await expandUnits("Only 1km left", "*");
|
|
67
|
+
* // "Only 1 kilometer left"
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export async function expandUnits(text, _textType) {
|
|
71
|
+
const replace = (_match, number, unit) => {
|
|
72
|
+
// Python indexes the map directly; every match is a key once lowercased.
|
|
73
|
+
const [singular, plural] = UNIT_MAP[unit.toLowerCase()];
|
|
74
|
+
// Only a bare "1" takes the singular; a decimal such as "1.0" reads as
|
|
75
|
+
// plural in speech.
|
|
76
|
+
return `${number} ${number === "1" ? singular : plural}`;
|
|
77
|
+
};
|
|
78
|
+
text = text.replace(UNIT_RE, replace);
|
|
79
|
+
return text.replace(AMBIGUOUS_UNIT_RE, replace);
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=units.js.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configurable voice formatting bundle for TTS preprocessing.
|
|
3
|
+
*
|
|
4
|
+
* Porting notes: upstream imports each transform lazily inside `__init__`;
|
|
5
|
+
* here they are imported statically (they have no optional dependencies).
|
|
6
|
+
*/
|
|
7
|
+
import type { TextType } from "../../../frames/frames.ts";
|
|
8
|
+
import { type TextReplacement } from "./replacements.ts";
|
|
9
|
+
type Transform = (text: string, textType: string | TextType) => Promise<string>;
|
|
10
|
+
export interface VoiceFormatterOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Strip Markdown formatting symbols (bold, italic, headers, code spans).
|
|
13
|
+
* Enabled by default.
|
|
14
|
+
*/
|
|
15
|
+
stripMarkdown?: boolean;
|
|
16
|
+
/** Space out phone number digits for individual pronunciation. Enabled by default. */
|
|
17
|
+
expandPhoneNumbers?: boolean;
|
|
18
|
+
/** Space out uppercase acronyms (e.g. `"API"` → `"A P I"`). Enabled by default. */
|
|
19
|
+
normalizeAcronyms?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Expand currency amounts to spoken form (e.g. `"$42.50"` →
|
|
22
|
+
* `"forty two dollars and fifty cents"`). Requires `num2words`.
|
|
23
|
+
*/
|
|
24
|
+
expandCurrency?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Expand numeric digits to spoken words. Disabled by default since it can
|
|
27
|
+
* affect numbers that are better read as digits. Requires `num2words`.
|
|
28
|
+
*/
|
|
29
|
+
expandNumbers?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Numbers above this value are read digit-by-digit instead of as a
|
|
32
|
+
* quantity. Defaults to null (expand all numbers as words). Only used when
|
|
33
|
+
* `expandNumbers` is true.
|
|
34
|
+
*/
|
|
35
|
+
numberDigitCutoff?: number | bigint | null;
|
|
36
|
+
/** Expand percentage expressions (e.g. `"50%"` → `"fifty percent"`). Requires `num2words`. */
|
|
37
|
+
expandPercentages?: boolean;
|
|
38
|
+
/** Expand unit abbreviations (e.g. `"5km"` → `"5 kilometers"`). Enabled by default. */
|
|
39
|
+
expandUnits?: boolean;
|
|
40
|
+
/** Transform email addresses to spoken form. Enabled by default. */
|
|
41
|
+
emailToSpeech?: boolean;
|
|
42
|
+
/** Expand date expressions to spoken form. Requires `num2words`. */
|
|
43
|
+
normalizeDates?: boolean;
|
|
44
|
+
/** List of `(regexPattern, replacement)` pairs applied after all other transforms. */
|
|
45
|
+
customReplacements?: readonly TextReplacement[] | null;
|
|
46
|
+
}
|
|
47
|
+
export interface VoiceFormatter {
|
|
48
|
+
(text: string, textType: string | TextType): Promise<string>;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Configurable bundle that applies a pipeline of voice-formatting transforms.
|
|
52
|
+
*
|
|
53
|
+
* Each option enables or disables one transform. Transforms are applied in a
|
|
54
|
+
* deliberate order: structural cleanup first, language expansions second, user
|
|
55
|
+
* replacements last.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* const formatter = new VoiceFormatter({
|
|
60
|
+
* stripMarkdown: true,
|
|
61
|
+
* expandCurrency: true,
|
|
62
|
+
* numberDigitCutoff: 2025,
|
|
63
|
+
* customReplacements: [[/\bDr\./, "Doctor"]],
|
|
64
|
+
* });
|
|
65
|
+
* const tts = new CartesiaTTSService({
|
|
66
|
+
* textTransforms: [["*", formatter]],
|
|
67
|
+
* });
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export declare class VoiceFormatter {
|
|
71
|
+
protected _transforms: Transform[];
|
|
72
|
+
/** Initialize the voice formatter. */
|
|
73
|
+
constructor(options?: VoiceFormatterOptions);
|
|
74
|
+
/**
|
|
75
|
+
* Apply all configured transforms in order.
|
|
76
|
+
*
|
|
77
|
+
* Python's `__call__`; calling the instance runs this.
|
|
78
|
+
*
|
|
79
|
+
* @param text Input text to transform.
|
|
80
|
+
* @param textType Text type passed through to each transform.
|
|
81
|
+
* @returns Transformed text ready for TTS synthesis.
|
|
82
|
+
*/
|
|
83
|
+
transform(text: string, textType: string | TextType): Promise<string>;
|
|
84
|
+
}
|
|
85
|
+
export {};
|
|
86
|
+
//# sourceMappingURL=voice_formatter.d.ts.map
|