@juspay/neurolink 12.47.14 → 12.48.1
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/CHANGELOG.md +2 -2
- package/dist/auth/middleware/AuthMiddleware.d.ts +1 -1
- package/dist/browser/neurolink.min.js +578 -565
- package/dist/cli/factories/commandFactory.d.ts +7 -0
- package/dist/cli/factories/commandFactory.js +26 -0
- package/dist/cli/loop/optionsSchema.js +9 -0
- package/dist/context/budgetChecker.js +54 -2
- package/dist/context/contextDecision.js +2 -0
- package/dist/context/toolPairRepair.d.ts +12 -0
- package/dist/context/toolPairRepair.js +29 -3
- package/dist/core/baseProvider.d.ts +92 -3
- package/dist/core/baseProvider.js +254 -10
- package/dist/core/conversationMemoryManager.js +17 -2
- package/dist/core/loopEngine.js +1 -1
- package/dist/core/modules/MessageBuilder.js +11 -3
- package/dist/core/modules/TelemetryHandler.d.ts +2 -2
- package/dist/core/modules/ToolsManager.d.ts +35 -0
- package/dist/core/modules/ToolsManager.js +159 -55
- package/dist/core/nativeGenerateLoop.d.ts +58 -1
- package/dist/core/nativeGenerateLoop.js +277 -3
- package/dist/core/redisConversationMemoryManager.js +10 -3
- package/dist/core/resolveRequestKind.d.ts +24 -4
- package/dist/core/resolveRequestKind.js +31 -4
- package/dist/core/toolExecutionRecorder.d.ts +6 -0
- package/dist/core/toolExecutionRecorder.js +24 -2
- package/dist/core/toolRoutingDecision.js +1 -0
- package/dist/factories/mediaHandlerCatalog.js +5 -0
- package/dist/factories/providerDescriptors.js +15 -0
- package/dist/files/fileTools.d.ts +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +9 -2
- package/dist/memory/condensationPrompt.d.ts +8 -0
- package/dist/memory/condensationPrompt.js +29 -0
- package/dist/memory/hippocampusInitializer.d.ts +14 -2
- package/dist/memory/hippocampusInitializer.js +64 -3
- package/dist/neurolink.d.ts +252 -3
- package/dist/neurolink.js +898 -43
- package/dist/providers/amazonBedrock/client.js +6 -0
- package/dist/providers/amazonSagemaker.js +14 -1
- package/dist/providers/anthropic/client.js +148 -19
- package/dist/providers/anthropic/loopAdapter.js +2 -2
- package/dist/providers/googleAiStudio/client.js +2 -1
- package/dist/providers/openaiChatCompletionsBase.js +103 -23
- package/dist/providers/systemOneDecision.d.ts +6 -0
- package/dist/providers/systemOneDecision.js +14 -63
- package/dist/providers/typesafe.d.ts +6 -0
- package/dist/providers/typesafe.js +6 -0
- package/dist/rag/retrieval/searchDecision.js +1 -0
- package/dist/routing/classifierRouter.js +6 -5
- package/dist/routing/classifierStrategies.js +8 -1
- package/dist/types/classifierRouter.d.ts +16 -3
- package/dist/types/common.d.ts +2 -0
- package/dist/types/config.d.ts +9 -0
- package/dist/types/context.d.ts +8 -1
- package/dist/types/conversation.d.ts +87 -0
- package/dist/types/decision.d.ts +210 -1
- package/dist/types/decision.js +9 -1
- package/dist/types/dispatch.d.ts +11 -4
- package/dist/types/generate.d.ts +118 -54
- package/dist/types/loopEngine.d.ts +7 -2
- package/dist/types/memory.d.ts +177 -9
- package/dist/types/openaiCompatible.d.ts +11 -3
- package/dist/types/stream.d.ts +13 -16
- package/dist/types/stt.d.ts +58 -1
- package/dist/types/tools.d.ts +14 -0
- package/dist/types/tools.js +8 -0
- package/dist/types/tts.d.ts +108 -0
- package/dist/types/voice.d.ts +41 -4
- package/dist/utils/decisionAnswers.d.ts +31 -1
- package/dist/utils/decisionAnswers.js +114 -0
- package/dist/utils/decisionLimits.d.ts +39 -0
- package/dist/utils/decisionLimits.js +116 -0
- package/dist/utils/logger.d.ts +7 -0
- package/dist/utils/logger.js +9 -0
- package/dist/utils/messageBuilder.d.ts +45 -1
- package/dist/utils/messageBuilder.js +393 -16
- package/dist/utils/multimodalOptionsBuilder.d.ts +1 -0
- package/dist/utils/multimodalOptionsBuilder.js +1 -0
- package/dist/utils/speechText.d.ts +41 -0
- package/dist/utils/speechText.js +364 -0
- package/dist/utils/toolResultStatus.d.ts +26 -0
- package/dist/utils/toolResultStatus.js +73 -0
- package/dist/utils/ttsProcessor.d.ts +29 -0
- package/dist/utils/ttsProcessor.js +84 -2
- package/dist/voice/index.d.ts +1 -0
- package/dist/voice/index.js +3 -0
- package/dist/voice/providers/ElevenLabsSTT.d.ts +42 -0
- package/dist/voice/providers/ElevenLabsSTT.js +351 -0
- package/dist/voice/providers/ElevenLabsTTS.d.ts +81 -1
- package/dist/voice/providers/ElevenLabsTTS.js +415 -74
- package/docs-site/static/search-index.json +48 -33
- package/package.json +5 -2
- package/dist/utils/toolEndEmitter.d.ts +0 -25
- package/dist/utils/toolEndEmitter.js +0 -64
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text preparation for speech synthesis.
|
|
3
|
+
*
|
|
4
|
+
* Model output is written for a screen: markdown structure, URLs, emoji.
|
|
5
|
+
* Read aloud, every one of those is noise — "asterisk asterisk", a 90-character
|
|
6
|
+
* URL spelled letter by letter, "grinning face with smiling eyes". This module
|
|
7
|
+
* rewrites such text into what a voice should actually say.
|
|
8
|
+
*
|
|
9
|
+
* Every regular expression here is linear-time by construction: no nested
|
|
10
|
+
* quantifiers, no lazy scan to a closing token, and no unbounded greedy class
|
|
11
|
+
* followed by a required literal (the `[ \t]+\n` shape that goes quadratic on
|
|
12
|
+
* a long run of spaces). Where a closing token is required, the scan between
|
|
13
|
+
* the two is bounded AND excludes the opener's own character, so an unclosed
|
|
14
|
+
* opener fails at the next opener rather than walking the bound — a run of
|
|
15
|
+
* `[` costs one step per character, not one thousand. Block-level structure
|
|
16
|
+
* (fenced code, lists, tables, headings, blockquotes) is a line pass that
|
|
17
|
+
* toggles state, so an unterminated fence costs one scan rather than a
|
|
18
|
+
* backtracking search.
|
|
19
|
+
*
|
|
20
|
+
* @module utils/speechText
|
|
21
|
+
*/
|
|
22
|
+
// A namespace import, not a named one: the browser bundle replaces every
|
|
23
|
+
// `node:` module with a fixed stub that has no `domainToUnicode`, and a named
|
|
24
|
+
// import of a missing export fails that build. `hostnameOf` checks for the
|
|
25
|
+
// function at runtime and falls back to the punycode form.
|
|
26
|
+
import * as nodeUrl from "node:url";
|
|
27
|
+
const DEFAULT_CODE_BLOCK_PHRASE = "Code block omitted.";
|
|
28
|
+
/** A fence opener/closer: up to three spaces of indent, then ``` or ~~~. */
|
|
29
|
+
const FENCE_LINE = /^[ \t]{0,3}(?:`{3,}|~{3,})/;
|
|
30
|
+
/** ATX heading marker: `#` to `######` followed by whitespace. */
|
|
31
|
+
const HEADING_MARKER = /^[ \t]{0,3}#{1,6}[ \t]+/;
|
|
32
|
+
/** A line made only of `=`, `-`, `*`, `_` and spaces (setext underline or thematic break). */
|
|
33
|
+
const RULE_LINE = /^[ \t]{0,3}[=\-*_][=\-*_ \t]*$/;
|
|
34
|
+
/** A line made only of pipes, dashes, colons and spaces (table separator row). */
|
|
35
|
+
const TABLE_SEPARATOR_CHARS = /^[|:\- \t]+$/;
|
|
36
|
+
/** Unordered list bullet. */
|
|
37
|
+
const BULLET_MARKER = /^([ \t]*)[-*+][ \t]+/;
|
|
38
|
+
/** Ordered list marker (`1.` / `1)`), bounded to nine digits. */
|
|
39
|
+
const ORDERED_MARKER = /^([ \t]*)\d{1,9}[.)][ \t]+/;
|
|
40
|
+
/** Task-list checkbox left after a bullet is stripped. */
|
|
41
|
+
const TASK_CHECKBOX = /^\[[ xX]\][ \t]+/;
|
|
42
|
+
/** One level of blockquote. */
|
|
43
|
+
const BLOCKQUOTE_MARKER = /^[ \t]{0,3}>[ \t]?/;
|
|
44
|
+
// Inline constructs need a closing token. The span between opener and closer
|
|
45
|
+
// is bounded so an unclosed opener never scans to the end of the document.
|
|
46
|
+
// The bound alone is not enough: a label class that admits the opener's own
|
|
47
|
+
// character makes every `[` in a run of `[` walk the full bound before it
|
|
48
|
+
// fails, which is linear with a constant of LINK_TEXT_MAX — 2.6 s per
|
|
49
|
+
// megabyte of `[`. Excluding the opener from the class makes the failure
|
|
50
|
+
// immediate, so a run of openers costs one step each.
|
|
51
|
+
const LINK_TEXT_MAX = 1000;
|
|
52
|
+
const LINK_TARGET_MAX = 2048;
|
|
53
|
+
const CODE_SPAN_MAX = 1000;
|
|
54
|
+
const TAG_BODY_MAX = 500;
|
|
55
|
+
const EMPHASIS_SPAN_MAX = 1000;
|
|
56
|
+
/** Inline image: `` → alt. */
|
|
57
|
+
const INLINE_IMAGE = new RegExp(`!\\[([^\\][\\n]{0,${LINK_TEXT_MAX}})\\]\\([^)[\\n]{0,${LINK_TARGET_MAX}}\\)`, "g");
|
|
58
|
+
/** Inline link: `[text](href)` → text. */
|
|
59
|
+
const INLINE_LINK = new RegExp(`\\[([^\\][\\n]{0,${LINK_TEXT_MAX}})\\]\\([^)[\\n]{0,${LINK_TARGET_MAX}}\\)`, "g");
|
|
60
|
+
/** Reference link: `[text][ref]` → text. */
|
|
61
|
+
const REFERENCE_LINK = new RegExp(`\\[([^\\][\\n]{0,${LINK_TEXT_MAX}})\\]\\[[^\\][\\n]{0,${LINK_TEXT_MAX}}\\]`, "g");
|
|
62
|
+
/**
|
|
63
|
+
* Autolink: `<https://…>` → the bare URL, so the URL pass sees it. The target
|
|
64
|
+
* class excludes `<` for the same reason the link classes exclude `[`: an
|
|
65
|
+
* unterminated `<http://` used to walk the full target bound before failing,
|
|
66
|
+
* which made 1 MB of them cost ~380 ms instead of under 1 ms.
|
|
67
|
+
*/
|
|
68
|
+
const AUTOLINK = new RegExp(`<(https?://[^<>\\s]{1,${LINK_TARGET_MAX}})>`, "g");
|
|
69
|
+
/** Inline code span: the backticks go, the contents stay. */
|
|
70
|
+
const INLINE_CODE = new RegExp("`{1,3}([^`\\n]{0," + CODE_SPAN_MAX + "})`{1,3}", "g");
|
|
71
|
+
/**
|
|
72
|
+
* An HTML tag, opening or closing — but only a tag whose name is HTML. Model
|
|
73
|
+
* output uses angle brackets for placeholders (`<Enter>`, `<order-id>`),
|
|
74
|
+
* generics (`Map<string, T>`) and comparisons (`x<y and y>z`), and none of
|
|
75
|
+
* those may be swallowed: the word inside is usually the instruction.
|
|
76
|
+
*/
|
|
77
|
+
const HTML_TAG_NAMES = "p|br|b|i|u|em|strong|a|span|div|code|pre|ul|ol|li|h[1-6]|img|table|thead|tbody|tr|td|th|blockquote|hr|sup|sub|small|mark|del|ins|s";
|
|
78
|
+
const HTML_TAG = new RegExp(`</?(?:${HTML_TAG_NAMES})(?=[\\s/>])[^<>\\n]{0,${TAG_BODY_MAX}}>`, "gi");
|
|
79
|
+
/** Strikethrough markers abutting text. */
|
|
80
|
+
const STRIKETHROUGH = /~~(?=\S)|(?<=\S)~~/g;
|
|
81
|
+
// Emphasis is a PAIR: an opening run preceded by the start of the text,
|
|
82
|
+
// whitespace, punctuation or a symbol and followed by a non-space, then a
|
|
83
|
+
// closing run preceded by a non-space and followed by the end, whitespace,
|
|
84
|
+
// punctuation or a symbol. A `*` between two digits (`2*3*4`, `*123*1#`)
|
|
85
|
+
// satisfies neither side and is left alone — the old "any run touching a
|
|
86
|
+
// non-space" rule spoke `2*3*4 = 24` as "234 = 24". The span between the
|
|
87
|
+
// runs excludes the delimiter, so an unpaired opener fails at the next
|
|
88
|
+
// delimiter instead of scanning to the bound.
|
|
89
|
+
//
|
|
90
|
+
// The boundary classes are Unicode: `\p{P}` and `\p{S}` rather than a list
|
|
91
|
+
// of ASCII brackets and sentence marks. An ASCII list left `**` in Hindi
|
|
92
|
+
// (`**भुगतान सफल रहा**।` — the closer sits before a danda), Chinese
|
|
93
|
+
// (`**总结**:` — a full-width colon), curly quotes (`“**Quoted**”`) and
|
|
94
|
+
// before an em dash (`**Fast**—and`); for a voice assistant those are the
|
|
95
|
+
// common cases, not the edge ones. The delimiter is itself punctuation, so
|
|
96
|
+
// it is excluded from its own boundary: without that, `x**2 + y**2` read the
|
|
97
|
+
// second `*` of each run as an opener/closer and became `x*2 + y*2`.
|
|
98
|
+
//
|
|
99
|
+
// Han, Hiragana, Katakana and Hangul also count as a boundary: those scripts
|
|
100
|
+
// have no spaces between words, so `**` glued straight to surrounding CJK
|
|
101
|
+
// text (`这是**重点**内容`, not `这是 **重点** 内容`) is the normal way it is
|
|
102
|
+
// written, not an edge case — without this, that `**` had neither side
|
|
103
|
+
// satisfy the boundary and was spoken aloud as asterisks.
|
|
104
|
+
const EMPHASIS_BOUNDARY = "\\s\\p{White_Space}\\p{P}\\p{S}\\p{Script=Han}\\p{Script=Hiragana}\\p{Script=Katakana}\\p{Script=Hangul}";
|
|
105
|
+
function emphasisPattern(delimiter) {
|
|
106
|
+
const openBefore = `(?<![^${EMPHASIS_BOUNDARY}]|${delimiter})`;
|
|
107
|
+
const closeAfter = `(?![^${EMPHASIS_BOUNDARY}]|${delimiter})`;
|
|
108
|
+
const run = `${delimiter}{1,3}`;
|
|
109
|
+
const span = `([^${delimiter}\\n]{1,${EMPHASIS_SPAN_MAX}}?)`;
|
|
110
|
+
return new RegExp(`${openBefore}${run}(?=[^\\s${delimiter}])${span}(?<=\\S)${run}${closeAfter}`, "gu");
|
|
111
|
+
}
|
|
112
|
+
const EMPHASIS_ASTERISK = emphasisPattern("\\*");
|
|
113
|
+
const EMPHASIS_UNDERSCORE = emphasisPattern("_");
|
|
114
|
+
/** `**bold *and italic* inside**`: each pass peels one level, so nesting needs more than one. */
|
|
115
|
+
const EMPHASIS_PASSES = 3;
|
|
116
|
+
// Backslash escapes must be resolved BEFORE the emphasis and link passes —
|
|
117
|
+
// `\*not bold\*` is literal asterisks, not emphasis — and the resolved
|
|
118
|
+
// character must then survive those passes. Each escapable character is
|
|
119
|
+
// swapped for a sentinel first and swapped back last. The sentinels are
|
|
120
|
+
// Unicode noncharacters (U+FDD0–U+FDEF), which are reserved for exactly this
|
|
121
|
+
// process-internal use and never occur in interchanged text; any that do
|
|
122
|
+
// arrive are dropped so they cannot masquerade as an escape.
|
|
123
|
+
const ESCAPABLE_PUNCTUATION = "\\`*_{}[]()#+-.!|>~<";
|
|
124
|
+
const ESCAPED_PUNCTUATION = /\\([\\`*_{}[\]()#+\-.!|>~<])/g;
|
|
125
|
+
const ESCAPE_SENTINEL_BASE = 0xfdd0;
|
|
126
|
+
const ESCAPE_SENTINELS = /[-]/g;
|
|
127
|
+
/** An absolute http(s) URL, as one unbroken token. */
|
|
128
|
+
const HTTP_URL = /https?:\/\/[^\s<>()[\]"']+/gi;
|
|
129
|
+
/** A bare `www.` host with optional path. */
|
|
130
|
+
const WWW_URL = /\bwww\.[^\s<>()[\]"']+/gi;
|
|
131
|
+
const TRAILING_PUNCTUATION = new Set([".", ",", ";", ":", "!", "?"]);
|
|
132
|
+
// One pictograph with the modifiers, variation selectors and keycap combiner
|
|
133
|
+
// that may trail it, then any number of further pictographs joined to it by
|
|
134
|
+
// U+200D. The whole sequence goes as one unit so a joined family or a
|
|
135
|
+
// skin-toned hand leaves nothing behind. The joiner is consumed ONLY here,
|
|
136
|
+
// between or beside pictographs: U+200D is also a letter-shaping control in
|
|
137
|
+
// Devanagari, Malayalam, Arabic and others, and stripping it everywhere
|
|
138
|
+
// changed the shape and pronunciation of ordinary Indic text.
|
|
139
|
+
const PICTOGRAPH = "(?:\\p{Extended_Pictographic}|\\p{Emoji_Presentation})(?:\\p{Emoji_Modifier}|\\uFE0E|\\uFE0F|\\u20E3)*";
|
|
140
|
+
const EMOJI = new RegExp(`\\u200D?${PICTOGRAPH}(?:\\u200D${PICTOGRAPH})*\\u200D?`, "gu");
|
|
141
|
+
/** Skin-tone modifiers, variation selectors and the keycap combiner left over after a bare digit or symbol. */
|
|
142
|
+
const EMOJI_JOINERS = /\p{Emoji_Modifier}|\uFE0E|\uFE0F|\u20E3/gu;
|
|
143
|
+
/** An `http`/`https` scheme, matched case-insensitively (RFC 3986 \u00A73.1). */
|
|
144
|
+
const HTTP_SCHEME = /^https?:\/\//i;
|
|
145
|
+
const LINE_BREAKS = /\r\n?/g;
|
|
146
|
+
/** Runs of two or more horizontal spaces; nothing is required after the run. */
|
|
147
|
+
const HORIZONTAL_RUNS = /[ \t]{2,}/g;
|
|
148
|
+
/** Applied only after HORIZONTAL_RUNS, when no run is longer than one character. */
|
|
149
|
+
const SPACE_BEFORE_NEWLINE = /[ \t]\n/g;
|
|
150
|
+
const SPACE_AFTER_NEWLINE = /\n[ \t]/g;
|
|
151
|
+
const BLANK_LINE_RUNS = /\n{3,}/g;
|
|
152
|
+
function resolveOptions(opts) {
|
|
153
|
+
return {
|
|
154
|
+
markdown: opts?.markdown ?? true,
|
|
155
|
+
codeBlocks: opts?.codeBlocks ?? "phrase",
|
|
156
|
+
codeBlockPhrase: opts?.codeBlockPhrase ?? DEFAULT_CODE_BLOCK_PHRASE,
|
|
157
|
+
urls: opts?.urls ?? "hostname",
|
|
158
|
+
emoji: opts?.emoji ?? true,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Resolve a `TTSOptions.sanitize` value into options, or `undefined` when the
|
|
163
|
+
* pass is off. `true` means the defaults; an object tunes them.
|
|
164
|
+
*/
|
|
165
|
+
export function resolveSpeechSanitizeOptions(sanitize) {
|
|
166
|
+
if (sanitize === undefined || sanitize === false) {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
return sanitize === true ? {} : sanitize;
|
|
170
|
+
}
|
|
171
|
+
function isRuleLine(line) {
|
|
172
|
+
if (!RULE_LINE.test(line)) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
let markers = 0;
|
|
176
|
+
for (const ch of line) {
|
|
177
|
+
if (ch === "=" || ch === "-" || ch === "*" || ch === "_") {
|
|
178
|
+
markers += 1;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return markers >= 3;
|
|
182
|
+
}
|
|
183
|
+
function isTableSeparator(line) {
|
|
184
|
+
return line.includes("---") && TABLE_SEPARATOR_CHARS.test(line);
|
|
185
|
+
}
|
|
186
|
+
function isTableRow(line) {
|
|
187
|
+
const trimmed = line.trim();
|
|
188
|
+
return trimmed.startsWith("|") || trimmed.endsWith("|");
|
|
189
|
+
}
|
|
190
|
+
/** `| a | b |` → `a, b`. Split on the pipe and trim each cell — no regex over the padding. */
|
|
191
|
+
function flattenTableRow(line) {
|
|
192
|
+
const cells = [];
|
|
193
|
+
for (const cell of line.split("|")) {
|
|
194
|
+
const trimmed = cell.trim();
|
|
195
|
+
if (trimmed.length > 0) {
|
|
196
|
+
cells.push(trimmed);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return cells.join(", ");
|
|
200
|
+
}
|
|
201
|
+
function stripBlockquote(line) {
|
|
202
|
+
let current = line;
|
|
203
|
+
for (;;) {
|
|
204
|
+
const next = current.replace(BLOCKQUOTE_MARKER, "");
|
|
205
|
+
if (next === current) {
|
|
206
|
+
return current;
|
|
207
|
+
}
|
|
208
|
+
current = next;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* One pass over the lines: fenced code per `codeBlocks`, then the per-line
|
|
213
|
+
* block markers (headings, rules, bullets, blockquotes, tables).
|
|
214
|
+
*/
|
|
215
|
+
function stripBlockMarkdown(text, options) {
|
|
216
|
+
const out = [];
|
|
217
|
+
let inFence = false;
|
|
218
|
+
for (const line of text.split("\n")) {
|
|
219
|
+
if (FENCE_LINE.test(line)) {
|
|
220
|
+
if (options.codeBlocks === "keep") {
|
|
221
|
+
// Streaming: the block's extent is unknowable per segment, so only
|
|
222
|
+
// the fence line itself is dropped and the contents are spoken.
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (!inFence) {
|
|
226
|
+
inFence = true;
|
|
227
|
+
if (options.codeBlocks === "phrase") {
|
|
228
|
+
out.push(options.codeBlockPhrase);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
inFence = false;
|
|
233
|
+
}
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (inFence) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (isRuleLine(line) || isTableSeparator(line)) {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
let current = stripBlockquote(line);
|
|
243
|
+
current = current.replace(HEADING_MARKER, "");
|
|
244
|
+
current = current.replace(BULLET_MARKER, "$1");
|
|
245
|
+
current = current.replace(ORDERED_MARKER, "$1");
|
|
246
|
+
current = current.replace(TASK_CHECKBOX, "");
|
|
247
|
+
if (isTableRow(current)) {
|
|
248
|
+
current = flattenTableRow(current);
|
|
249
|
+
}
|
|
250
|
+
out.push(current);
|
|
251
|
+
}
|
|
252
|
+
return out.join("\n");
|
|
253
|
+
}
|
|
254
|
+
/** `\*` → sentinel, so the character it protects is invisible to the passes that follow. */
|
|
255
|
+
function protectEscapes(text) {
|
|
256
|
+
return text
|
|
257
|
+
.replace(ESCAPE_SENTINELS, "")
|
|
258
|
+
.replace(ESCAPED_PUNCTUATION, (_match, ch) => String.fromCharCode(ESCAPE_SENTINEL_BASE + ESCAPABLE_PUNCTUATION.indexOf(ch)));
|
|
259
|
+
}
|
|
260
|
+
/** Sentinel → the literal character `\*` stood for. */
|
|
261
|
+
function restoreEscapes(text) {
|
|
262
|
+
return text.replace(ESCAPE_SENTINELS, (sentinel) => ESCAPABLE_PUNCTUATION[sentinel.charCodeAt(0) - ESCAPE_SENTINEL_BASE] ??
|
|
263
|
+
"");
|
|
264
|
+
}
|
|
265
|
+
function stripEmphasis(text) {
|
|
266
|
+
let current = text;
|
|
267
|
+
for (let pass = 0; pass < EMPHASIS_PASSES; pass++) {
|
|
268
|
+
const next = current
|
|
269
|
+
.replace(EMPHASIS_ASTERISK, "$1")
|
|
270
|
+
.replace(EMPHASIS_UNDERSCORE, "$1");
|
|
271
|
+
if (next === current) {
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
current = next;
|
|
275
|
+
}
|
|
276
|
+
return current;
|
|
277
|
+
}
|
|
278
|
+
function stripInlineMarkdown(text) {
|
|
279
|
+
const stripped = protectEscapes(text)
|
|
280
|
+
.replace(INLINE_IMAGE, "$1")
|
|
281
|
+
.replace(INLINE_LINK, "$1")
|
|
282
|
+
.replace(REFERENCE_LINK, "$1")
|
|
283
|
+
.replace(AUTOLINK, "$1")
|
|
284
|
+
.replace(INLINE_CODE, "$1")
|
|
285
|
+
.replace(HTML_TAG, " ")
|
|
286
|
+
.replace(STRIKETHROUGH, "");
|
|
287
|
+
return restoreEscapes(stripEmphasis(stripped));
|
|
288
|
+
}
|
|
289
|
+
/** Split a URL token into the URL proper and the sentence punctuation glued to its end. */
|
|
290
|
+
function splitTrailingPunctuation(token) {
|
|
291
|
+
let end = token.length;
|
|
292
|
+
while (end > 0 && TRAILING_PUNCTUATION.has(token[end - 1] ?? "")) {
|
|
293
|
+
end -= 1;
|
|
294
|
+
}
|
|
295
|
+
return { url: token.slice(0, end), trailing: token.slice(end) };
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* The host a URL should be spoken as. `URL.hostname` is the punycode form
|
|
299
|
+
* (`xn--mnchen-3ya.example`), which a voice spells letter by letter; Node's
|
|
300
|
+
* `domainToUnicode` gives back `münchen.example`. The scheme test is
|
|
301
|
+
* case-insensitive because `HTTP_URL` is: `HTTPS://Example.COM/x` used to
|
|
302
|
+
* fail a case-sensitive `startsWith("http")`, get a second scheme prepended,
|
|
303
|
+
* and be spoken as "https".
|
|
304
|
+
*/
|
|
305
|
+
function hostnameOf(url) {
|
|
306
|
+
const withScheme = HTTP_SCHEME.test(url) ? url : `https://${url}`;
|
|
307
|
+
try {
|
|
308
|
+
const host = new URL(withScheme).hostname;
|
|
309
|
+
return typeof nodeUrl.domainToUnicode === "function"
|
|
310
|
+
? nodeUrl.domainToUnicode(host)
|
|
311
|
+
: host;
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
return "";
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function rewriteUrls(text, mode) {
|
|
318
|
+
const replacer = (match) => {
|
|
319
|
+
const { url, trailing } = splitTrailingPunctuation(match);
|
|
320
|
+
if (mode === "remove") {
|
|
321
|
+
return trailing;
|
|
322
|
+
}
|
|
323
|
+
const host = hostnameOf(url);
|
|
324
|
+
return host ? `${host}${trailing}` : trailing;
|
|
325
|
+
};
|
|
326
|
+
return text.replace(HTTP_URL, replacer).replace(WWW_URL, replacer);
|
|
327
|
+
}
|
|
328
|
+
function removeEmoji(text) {
|
|
329
|
+
return text.replace(EMOJI, "").replace(EMOJI_JOINERS, "");
|
|
330
|
+
}
|
|
331
|
+
function collapseWhitespace(text) {
|
|
332
|
+
return text
|
|
333
|
+
.replace(HORIZONTAL_RUNS, " ")
|
|
334
|
+
.replace(SPACE_BEFORE_NEWLINE, "\n")
|
|
335
|
+
.replace(SPACE_AFTER_NEWLINE, "\n")
|
|
336
|
+
.replace(BLANK_LINE_RUNS, "\n\n")
|
|
337
|
+
.trim();
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Rewrite `text` into what a TTS voice should say.
|
|
341
|
+
*
|
|
342
|
+
* Pure and deterministic: the same input and options always yield the same
|
|
343
|
+
* output, and nothing here reads the environment. Runs in time linear in the
|
|
344
|
+
* length of `text`.
|
|
345
|
+
*
|
|
346
|
+
* @example
|
|
347
|
+
* ```typescript
|
|
348
|
+
* prepareTextForSpeech("## Title\n\nSee **this** at https://docs.example.com/x 🎉");
|
|
349
|
+
* // → "Title\n\nSee this at docs.example.com"
|
|
350
|
+
* ```
|
|
351
|
+
*/
|
|
352
|
+
export function prepareTextForSpeech(text, opts) {
|
|
353
|
+
const options = resolveOptions(opts);
|
|
354
|
+
let out = text.replace(LINE_BREAKS, "\n");
|
|
355
|
+
if (options.markdown) {
|
|
356
|
+
out = stripBlockMarkdown(out, options);
|
|
357
|
+
out = stripInlineMarkdown(out);
|
|
358
|
+
}
|
|
359
|
+
out = rewriteUrls(out, options.urls);
|
|
360
|
+
if (options.emoji) {
|
|
361
|
+
out = removeEmoji(out);
|
|
362
|
+
}
|
|
363
|
+
return collapseWhitespace(out);
|
|
364
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classification of a tool result that the loop received as a normal return
|
|
3
|
+
* but that reports a failure in-band.
|
|
4
|
+
*
|
|
5
|
+
* `neurolink.executeTool` converts a thrown error and a circuit-breaker
|
|
6
|
+
* refusal into `{ isError: true, content: [...] }`, MCP servers return the
|
|
7
|
+
* same shape for a failed call, and the documented custom-tool pattern is
|
|
8
|
+
* `return { error: "User not found" }`. Every consumer that decides "did this
|
|
9
|
+
* tool succeed" — the `tool:end` event, the stored `tool_result` row, the
|
|
10
|
+
* replayed marker, and `toolExecutions[].isError` — has to read those shapes
|
|
11
|
+
* the same way, so the rule lives in ONE predicate, `isErrorShapedToolResult`,
|
|
12
|
+
* and this module only extracts the text to report for it.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* The error text carried by an error-shaped result (`isError: true`, an
|
|
16
|
+
* `error` field, or a failing `status`), or undefined when the value is not
|
|
17
|
+
* one. Prefers an `error` string, then a serialized non-string `error`, then
|
|
18
|
+
* the joined text parts of an MCP `content` array, then a fixed fallback.
|
|
19
|
+
*/
|
|
20
|
+
export declare function extractIsErrorText(output: unknown): string | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Same classification for a serialized result: the memory stores keep
|
|
23
|
+
* `tool_result.content` as a JSON string, so a row written before the store
|
|
24
|
+
* learned to flag error shapes can still be recognised at replay.
|
|
25
|
+
*/
|
|
26
|
+
export declare function extractIsErrorTextFromContent(content: unknown): string | undefined;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classification of a tool result that the loop received as a normal return
|
|
3
|
+
* but that reports a failure in-band.
|
|
4
|
+
*
|
|
5
|
+
* `neurolink.executeTool` converts a thrown error and a circuit-breaker
|
|
6
|
+
* refusal into `{ isError: true, content: [...] }`, MCP servers return the
|
|
7
|
+
* same shape for a failed call, and the documented custom-tool pattern is
|
|
8
|
+
* `return { error: "User not found" }`. Every consumer that decides "did this
|
|
9
|
+
* tool succeed" — the `tool:end` event, the stored `tool_result` row, the
|
|
10
|
+
* replayed marker, and `toolExecutions[].isError` — has to read those shapes
|
|
11
|
+
* the same way, so the rule lives in ONE predicate, `isErrorShapedToolResult`,
|
|
12
|
+
* and this module only extracts the text to report for it.
|
|
13
|
+
*/
|
|
14
|
+
import { isErrorShapedToolResult, isMeaningfulError, } from "../core/toolExecutionRecorder.js";
|
|
15
|
+
const ISERROR_FALLBACK_TEXT = "Tool returned isError: true";
|
|
16
|
+
const ERROR_SHAPE_FALLBACK_TEXT = "Tool reported an error";
|
|
17
|
+
/**
|
|
18
|
+
* The error text carried by an error-shaped result (`isError: true`, an
|
|
19
|
+
* `error` field, or a failing `status`), or undefined when the value is not
|
|
20
|
+
* one. Prefers an `error` string, then a serialized non-string `error`, then
|
|
21
|
+
* the joined text parts of an MCP `content` array, then a fixed fallback.
|
|
22
|
+
*/
|
|
23
|
+
export function extractIsErrorText(output) {
|
|
24
|
+
if (!isErrorShapedToolResult(output)) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
const record = output;
|
|
28
|
+
if (typeof record.error === "string" && record.error.length > 0) {
|
|
29
|
+
return record.error;
|
|
30
|
+
}
|
|
31
|
+
if (record.error !== true && isMeaningfulError(record.error)) {
|
|
32
|
+
try {
|
|
33
|
+
return JSON.stringify(record.error) ?? String(record.error);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return String(record.error);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (Array.isArray(record.content)) {
|
|
40
|
+
const texts = record.content
|
|
41
|
+
.filter((c) => c && c.type === "text" && typeof c.text === "string")
|
|
42
|
+
.map((c) => c.text);
|
|
43
|
+
if (texts.length > 0) {
|
|
44
|
+
return texts.join(" ");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return record.isError === true
|
|
48
|
+
? ISERROR_FALLBACK_TEXT
|
|
49
|
+
: ERROR_SHAPE_FALLBACK_TEXT;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Same classification for a serialized result: the memory stores keep
|
|
53
|
+
* `tool_result.content` as a JSON string, so a row written before the store
|
|
54
|
+
* learned to flag error shapes can still be recognised at replay.
|
|
55
|
+
*/
|
|
56
|
+
export function extractIsErrorTextFromContent(content) {
|
|
57
|
+
if (typeof content !== "string") {
|
|
58
|
+
return extractIsErrorText(content);
|
|
59
|
+
}
|
|
60
|
+
const trimmed = content.trim();
|
|
61
|
+
if (!trimmed.startsWith("{") ||
|
|
62
|
+
!(trimmed.includes('"isError"') ||
|
|
63
|
+
trimmed.includes('"error"') ||
|
|
64
|
+
trimmed.includes('"status"'))) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
return extractIsErrorText(JSON.parse(trimmed));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -20,6 +20,28 @@ export declare const TTS_ERROR_CODES: {
|
|
|
20
20
|
readonly SYNTHESIS_FAILED: "TTS_SYNTHESIS_FAILED";
|
|
21
21
|
readonly INVALID_INPUT: "TTS_INVALID_INPUT";
|
|
22
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* The wall-clock a handler may legitimately spend on one `synthesize()` call:
|
|
25
|
+
* `timeoutMs × (1 + retries)` plus pause slack (see `TTSHandlerTimeBudget`).
|
|
26
|
+
* A knob the caller did not set takes the handler default — 30 s per attempt
|
|
27
|
+
* and one retry — so the budget is always a number. It used to be `undefined`
|
|
28
|
+
* when neither knob was set, which left the outer synthesis timeout at the
|
|
29
|
+
* provider's own value: with the 60 s provider default, a first attempt that
|
|
30
|
+
* hung to its 30 s abort left no room for the documented default retry, and
|
|
31
|
+
* the caller got a timeout error instead of the retried audio.
|
|
32
|
+
*
|
|
33
|
+
* Used by `BaseProvider`'s generate() TTS paths so the outer `withTimeoutFn`
|
|
34
|
+
* never undercuts the handler's own retry loop.
|
|
35
|
+
*
|
|
36
|
+
* `provider` is the name the synthesis will actually be dispatched to —
|
|
37
|
+
* `tts.provider`, falling back to the request's or the instance's provider —
|
|
38
|
+
* not just `options.provider`: a caller that reaches ElevenLabs through the
|
|
39
|
+
* outer provider and leaves `tts.provider` unset got a budget of 0, and the
|
|
40
|
+
* outer timeout cut the handler's documented default retry. The registry
|
|
41
|
+
* resolves names case-insensitively, so this check does too; `"ElevenLabs"`
|
|
42
|
+
* found the handler but not the budget.
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolveTTSHandlerBudgetMs(options: TTSOptions, provider?: string): number;
|
|
23
45
|
/** Internal signal raised after all buffered segments have been attempted. */
|
|
24
46
|
export declare class IncrementalTTSSynthesisError extends Error {
|
|
25
47
|
readonly firstError: unknown;
|
|
@@ -74,6 +96,13 @@ export declare class TTSProcessor {
|
|
|
74
96
|
* @private
|
|
75
97
|
*/
|
|
76
98
|
private static readonly DEFAULT_MAX_TEXT_LENGTH;
|
|
99
|
+
/**
|
|
100
|
+
* The text cap `synthesize()` enforces for `providerName`: the handler's
|
|
101
|
+
* `maxTextLength`, or the default when the handler declares none or is not
|
|
102
|
+
* registered. Lets a caller bound work it does BEFORE synthesis — the
|
|
103
|
+
* sanitize pass in `BaseProvider` — to text that could ever be accepted.
|
|
104
|
+
*/
|
|
105
|
+
static maxTextLengthFor(providerName: string): number;
|
|
77
106
|
/**
|
|
78
107
|
* Register a TTS handler for a specific provider
|
|
79
108
|
*
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { logger } from "./logger.js";
|
|
10
10
|
import { VALID_AUDIO_FORMATS } from "../types/index.js";
|
|
11
|
+
import { prepareTextForSpeech, resolveSpeechSanitizeOptions, } from "./speechText.js";
|
|
11
12
|
import { ErrorCategory, ErrorSeverity } from "../constants/enums.js";
|
|
12
13
|
import { NeuroLinkError } from "./errorHandling.js";
|
|
13
14
|
import { HandlerRegistry } from "../core/handlerRegistry.js";
|
|
@@ -26,6 +27,62 @@ export const TTS_ERROR_CODES = {
|
|
|
26
27
|
};
|
|
27
28
|
const DEFAULT_STREAMING_BUFFER_SIZE = 120;
|
|
28
29
|
const SENTENCE_BOUNDARY = /[.!?]+(?:["')\]]+)?(?=\s|$)/g;
|
|
30
|
+
/**
|
|
31
|
+
* Slack added to a handler's declared time budget for the pauses its retry
|
|
32
|
+
* loop takes between attempts (Retry-After is capped at 10 s per pause).
|
|
33
|
+
*/
|
|
34
|
+
const HANDLER_RETRY_PAUSE_SLACK_MS = 10_000;
|
|
35
|
+
/** The handler defaults the budget assumes when the caller set no knobs (ElevenLabs: 30 s, one retry). */
|
|
36
|
+
const HANDLER_DEFAULT_TIMEOUT_MS = 30_000;
|
|
37
|
+
const HANDLER_DEFAULT_RETRIES = 1;
|
|
38
|
+
/** Handlers with a built-in retry loop, i.e. the ones the defaults describe. */
|
|
39
|
+
const RETRYING_TTS_PROVIDERS = new Set([
|
|
40
|
+
"elevenlabs",
|
|
41
|
+
"elevenlabs-tts",
|
|
42
|
+
]);
|
|
43
|
+
/** Largest delay `setTimeout` honours; anything above fires at once. */
|
|
44
|
+
const MAX_TIMER_MS = 2_147_483_647;
|
|
45
|
+
/**
|
|
46
|
+
* The wall-clock a handler may legitimately spend on one `synthesize()` call:
|
|
47
|
+
* `timeoutMs × (1 + retries)` plus pause slack (see `TTSHandlerTimeBudget`).
|
|
48
|
+
* A knob the caller did not set takes the handler default — 30 s per attempt
|
|
49
|
+
* and one retry — so the budget is always a number. It used to be `undefined`
|
|
50
|
+
* when neither knob was set, which left the outer synthesis timeout at the
|
|
51
|
+
* provider's own value: with the 60 s provider default, a first attempt that
|
|
52
|
+
* hung to its 30 s abort left no room for the documented default retry, and
|
|
53
|
+
* the caller got a timeout error instead of the retried audio.
|
|
54
|
+
*
|
|
55
|
+
* Used by `BaseProvider`'s generate() TTS paths so the outer `withTimeoutFn`
|
|
56
|
+
* never undercuts the handler's own retry loop.
|
|
57
|
+
*
|
|
58
|
+
* `provider` is the name the synthesis will actually be dispatched to —
|
|
59
|
+
* `tts.provider`, falling back to the request's or the instance's provider —
|
|
60
|
+
* not just `options.provider`: a caller that reaches ElevenLabs through the
|
|
61
|
+
* outer provider and leaves `tts.provider` unset got a budget of 0, and the
|
|
62
|
+
* outer timeout cut the handler's documented default retry. The registry
|
|
63
|
+
* resolves names case-insensitively, so this check does too; `"ElevenLabs"`
|
|
64
|
+
* found the handler but not the budget.
|
|
65
|
+
*/
|
|
66
|
+
export function resolveTTSHandlerBudgetMs(options, provider) {
|
|
67
|
+
const { timeoutMs, retries } = options;
|
|
68
|
+
const hasTimeout = typeof timeoutMs === "number" &&
|
|
69
|
+
Number.isFinite(timeoutMs) &&
|
|
70
|
+
timeoutMs > 0;
|
|
71
|
+
const hasRetries = typeof retries === "number" && Number.isInteger(retries) && retries >= 0;
|
|
72
|
+
const resolvedProvider = String(provider ?? options.provider ?? "").toLowerCase();
|
|
73
|
+
// The defaults describe a retry loop. Only a handler that has one gets a
|
|
74
|
+
// default budget; for every other provider (Google, OpenAI, Azure,
|
|
75
|
+
// Cartesia — one attempt, no knobs) the budget is 0 so the caller's own
|
|
76
|
+
// `timeout` stays the bound instead of being raised to ~70 s.
|
|
77
|
+
if (!hasTimeout &&
|
|
78
|
+
!hasRetries &&
|
|
79
|
+
!RETRYING_TTS_PROVIDERS.has(resolvedProvider)) {
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
const perAttempt = Math.min(hasTimeout ? timeoutMs : HANDLER_DEFAULT_TIMEOUT_MS, MAX_TIMER_MS);
|
|
83
|
+
const attempts = 1 + (hasRetries ? retries : HANDLER_DEFAULT_RETRIES);
|
|
84
|
+
return Math.min(perAttempt * attempts + (attempts - 1) * HANDLER_RETRY_PAUSE_SLACK_MS, MAX_TIMER_MS);
|
|
85
|
+
}
|
|
29
86
|
/** Internal signal raised after all buffered segments have been attempted. */
|
|
30
87
|
export class IncrementalTTSSynthesisError extends Error {
|
|
31
88
|
firstError;
|
|
@@ -247,6 +304,16 @@ export class TTSProcessor {
|
|
|
247
304
|
* @private
|
|
248
305
|
*/
|
|
249
306
|
static DEFAULT_MAX_TEXT_LENGTH = 3000;
|
|
307
|
+
/**
|
|
308
|
+
* The text cap `synthesize()` enforces for `providerName`: the handler's
|
|
309
|
+
* `maxTextLength`, or the default when the handler declares none or is not
|
|
310
|
+
* registered. Lets a caller bound work it does BEFORE synthesis — the
|
|
311
|
+
* sanitize pass in `BaseProvider` — to text that could ever be accepted.
|
|
312
|
+
*/
|
|
313
|
+
static maxTextLengthFor(providerName) {
|
|
314
|
+
return (this.getHandler(providerName)?.maxTextLength ??
|
|
315
|
+
this.DEFAULT_MAX_TEXT_LENGTH);
|
|
316
|
+
}
|
|
250
317
|
/**
|
|
251
318
|
* Register a TTS handler for a specific provider
|
|
252
319
|
*
|
|
@@ -779,6 +846,13 @@ export class TTSProcessor {
|
|
|
779
846
|
const maxTextLength = Math.max(1, handler?.maxTextLength ?? processor.DEFAULT_MAX_TEXT_LENGTH);
|
|
780
847
|
const requestedBoundary = options.streamingBufferSize ?? DEFAULT_STREAMING_BUFFER_SIZE;
|
|
781
848
|
const flushBoundary = Math.min(Math.max(1, Math.trunc(requestedBoundary)), maxTextLength);
|
|
849
|
+
// Per-segment cleanup when `tts.sanitize` is on. A segment may hold an
|
|
850
|
+
// unterminated fence, so code blocks are always kept (fence lines
|
|
851
|
+
// stripped, contents spoken) regardless of the caller's `codeBlocks`.
|
|
852
|
+
const sanitizeOptions = resolveSpeechSanitizeOptions(options.sanitize);
|
|
853
|
+
const segmentSanitize = sanitizeOptions
|
|
854
|
+
? { ...sanitizeOptions, codeBlocks: "keep" }
|
|
855
|
+
: undefined;
|
|
782
856
|
let buffer = "";
|
|
783
857
|
let chunkIndex = 0;
|
|
784
858
|
let cumulativeSize = 0;
|
|
@@ -920,8 +994,8 @@ export class TTSProcessor {
|
|
|
920
994
|
};
|
|
921
995
|
try {
|
|
922
996
|
while (!stopped()) {
|
|
923
|
-
const
|
|
924
|
-
if (
|
|
997
|
+
const rawSegment = takeSegment();
|
|
998
|
+
if (rawSegment === undefined) {
|
|
925
999
|
if (inputComplete) {
|
|
926
1000
|
break;
|
|
927
1001
|
}
|
|
@@ -935,6 +1009,14 @@ export class TTSProcessor {
|
|
|
935
1009
|
}
|
|
936
1010
|
continue;
|
|
937
1011
|
}
|
|
1012
|
+
const segment = segmentSanitize
|
|
1013
|
+
? prepareTextForSpeech(rawSegment, segmentSanitize)
|
|
1014
|
+
: rawSegment;
|
|
1015
|
+
// A segment that was only markup (a fence line, a table separator,
|
|
1016
|
+
// a bare heading marker) has nothing to say; it is not a failure.
|
|
1017
|
+
if (segment.length === 0) {
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
938
1020
|
const currentSegment = ++segmentNumber;
|
|
939
1021
|
// NOTHING BELOW MAY `yield` INSIDE A `try` THAT HAS A `catch`.
|
|
940
1022
|
// A consumer can resume this generator with `AsyncGenerator.throw()`
|
package/dist/voice/index.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ export { FishAudioTTS, FishAudioTTS as FishAudioTTSHandler, } from "./providers/
|
|
|
32
32
|
export { OpenAITTS, OpenAITTS as OpenAITTSHandler, } from "./providers/OpenAITTS.js";
|
|
33
33
|
export { AzureSTT, AzureSTT as AzureSTTHandler } from "./providers/AzureSTT.js";
|
|
34
34
|
export { DeepgramSTT, DeepgramSTT as DeepgramSTTHandler, } from "./providers/DeepgramSTT.js";
|
|
35
|
+
export { ElevenLabsSTT, ElevenLabsSTT as ElevenLabsSTTHandler, } from "./providers/ElevenLabsSTT.js";
|
|
35
36
|
export { GoogleSTT, GoogleSTT as GoogleSTTHandler, } from "./providers/GoogleSTT.js";
|
|
36
37
|
export { OpenAISTT, OpenAISTTHandler, WhisperSTT, WhisperSTTHandler, } from "./providers/OpenAISTT.js";
|
|
37
38
|
export { GeminiLive, GeminiLive as GeminiLiveHandler, } from "./providers/GeminiLive.js";
|
package/dist/voice/index.js
CHANGED
|
@@ -61,6 +61,7 @@ export { OpenAITTS, OpenAITTS as OpenAITTSHandler, } from "./providers/OpenAITTS
|
|
|
61
61
|
// ============================================================================
|
|
62
62
|
export { AzureSTT, AzureSTT as AzureSTTHandler } from "./providers/AzureSTT.js";
|
|
63
63
|
export { DeepgramSTT, DeepgramSTT as DeepgramSTTHandler, } from "./providers/DeepgramSTT.js";
|
|
64
|
+
export { ElevenLabsSTT, ElevenLabsSTT as ElevenLabsSTTHandler, } from "./providers/ElevenLabsSTT.js";
|
|
64
65
|
export { GoogleSTT, GoogleSTT as GoogleSTTHandler, } from "./providers/GoogleSTT.js";
|
|
65
66
|
export { OpenAISTT, OpenAISTTHandler, WhisperSTT, WhisperSTTHandler, } from "./providers/OpenAISTT.js";
|
|
66
67
|
// ============================================================================
|
|
@@ -79,6 +80,7 @@ import { SixtyDBTTS } from "./providers/SixtyDBTTS.js";
|
|
|
79
80
|
import { OpenAITTS } from "./providers/OpenAITTS.js";
|
|
80
81
|
import { AzureSTT } from "./providers/AzureSTT.js";
|
|
81
82
|
import { DeepgramSTT } from "./providers/DeepgramSTT.js";
|
|
83
|
+
import { ElevenLabsSTT } from "./providers/ElevenLabsSTT.js";
|
|
82
84
|
import { GoogleSTT } from "./providers/GoogleSTT.js";
|
|
83
85
|
import { OpenAISTT } from "./providers/OpenAISTT.js";
|
|
84
86
|
import { GeminiLive } from "./providers/GeminiLive.js";
|
|
@@ -107,6 +109,7 @@ const STT_HANDLER_FACTORIES = {
|
|
|
107
109
|
deepgram: () => new DeepgramSTT(),
|
|
108
110
|
"google-stt": () => new GoogleSTT(),
|
|
109
111
|
"azure-stt": () => new AzureSTT(),
|
|
112
|
+
"elevenlabs-stt": () => new ElevenLabsSTT(),
|
|
110
113
|
};
|
|
111
114
|
const STT_HANDLER_CANDIDATES = MEDIA_HANDLER_CATALOG.filter((entry) => entry.kind === "stt").map((entry) => {
|
|
112
115
|
const factory = STT_HANDLER_FACTORIES[entry.name];
|