speechready 0.0.1 → 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.
@@ -0,0 +1,411 @@
1
+ // Generated by scripts/build-profiles.mjs from spec/profiles. Do not edit.
2
+ export const PROFILE_DATA = [
3
+ {
4
+ "id": "amazon-polly",
5
+ "provider": "polly",
6
+ "models": [
7
+ "neural",
8
+ "generative",
9
+ "long-form",
10
+ "standard"
11
+ ],
12
+ "default": true,
13
+ "syntax": "ssml",
14
+ "escape": "xml",
15
+ "audio_tags": false,
16
+ "limit": {
17
+ "unit": "chars",
18
+ "max": 3000
19
+ },
20
+ "intents": {
21
+ "pause": {
22
+ "render": "<break time=\"{ms}ms\"/>",
23
+ "min_ms": 0,
24
+ "max_ms": 10000
25
+ },
26
+ "spell": {
27
+ "render": "spaced"
28
+ },
29
+ "emphasis": {
30
+ "render": "{text}"
31
+ },
32
+ "phoneme": {
33
+ "render": "<phoneme alphabet=\"ipa\" ph=\"{ipa}\">{text}</phoneme>"
34
+ }
35
+ },
36
+ "lexicon": {
37
+ "format": "pls",
38
+ "alphabet": "ipa"
39
+ },
40
+ "notes": "Output is the SSML body for `TextType=ssml`; the caller wraps it in <speak>. SynthesizeSpeech takes at most 3,000 billed characters (6,000 including tags); the profile counts everything, so 3,000 is safe. <break> caps at 10 seconds. <emphasis> is unavailable on neural, long-form and generative voices, so emphasis is rendered as plain text. <say-as interpret-as=\"characters\"> silently switches a neural voice to its standard counterpart for that sentence, so spelling is rendered as spaced characters instead. <phoneme> with IPA is supported (partially on generative). Lexicons are uploaded as PLS with PutLexicon.\n",
41
+ "sources": [
42
+ "https://docs.aws.amazon.com/polly/latest/dg/API_SynthesizeSpeech.html",
43
+ "https://docs.aws.amazon.com/polly/latest/dg/supportedtags.html",
44
+ "https://docs.aws.amazon.com/polly/latest/dg/break-tag.html",
45
+ "https://docs.aws.amazon.com/polly/latest/dg/say-as-tag.html"
46
+ ],
47
+ "verified": "2026-10-29"
48
+ },
49
+ {
50
+ "id": "azure-neural",
51
+ "provider": "azure",
52
+ "models": [
53
+ "neural",
54
+ "hd"
55
+ ],
56
+ "default": true,
57
+ "syntax": "ssml",
58
+ "escape": "xml",
59
+ "audio_tags": false,
60
+ "limit": {
61
+ "unit": "chars",
62
+ "max": 8000
63
+ },
64
+ "intents": {
65
+ "pause": {
66
+ "render": "<break time=\"{ms}ms\" />",
67
+ "min_ms": 0,
68
+ "max_ms": 20000
69
+ },
70
+ "spell": {
71
+ "render": "<say-as interpret-as=\"characters\">{text}</say-as>"
72
+ },
73
+ "emphasis": {
74
+ "render": "<emphasis level=\"strong\">{text}</emphasis>"
75
+ },
76
+ "phoneme": {
77
+ "render": "<phoneme alphabet=\"ipa\" ph=\"{ipa}\">{text}</phoneme>"
78
+ }
79
+ },
80
+ "lexicon": {
81
+ "format": "pls",
82
+ "alphabet": "ipa"
83
+ },
84
+ "notes": "Output is the SSML body: text with break, say-as and emphasis elements, with & < > escaped. The caller wraps it in <speak> and <voice>, since the voice name is theirs to choose. Azure caps audio at 10 minutes per request and the WebSocket SSML message at 64 KB; 8,000 characters stays well inside both at normal speaking rates. HD voices support a subset of SSML; check the HD page.\n",
85
+ "sources": [
86
+ "https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-synthesis-markup-structure",
87
+ "https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-services-quotas-and-limits"
88
+ ],
89
+ "verified": "2026-10-28"
90
+ },
91
+ {
92
+ "id": "cartesia-sonic",
93
+ "provider": "cartesia",
94
+ "models": [
95
+ "sonic-3.6",
96
+ "sonic-3.5",
97
+ "sonic-3",
98
+ "sonic-latest",
99
+ "sonic-2",
100
+ "sonic-turbo"
101
+ ],
102
+ "default": true,
103
+ "syntax": "plain",
104
+ "escape": "none",
105
+ "audio_tags": false,
106
+ "limit": {
107
+ "unit": "chars",
108
+ "max": null
109
+ },
110
+ "intents": {
111
+ "pause": {
112
+ "render": "<break time=\"{ms}ms\"/>",
113
+ "min_ms": 0
114
+ },
115
+ "spell": {
116
+ "render": "<spell>{text}</spell>"
117
+ },
118
+ "emphasis": {
119
+ "render": "{text}"
120
+ }
121
+ },
122
+ "lexicon": {
123
+ "format": "none"
124
+ },
125
+ "notes": "Sonic takes plain text with a small set of SSML-like tags, not SSML: <break time=\"500ms\"/>, <spell>ABC123</spell>, and speed, volume and emotion tags on sonic-3 and newer. Each tag counts as one character. Cartesia publishes no per-request transcript limit; its own guidance for agents is one sentence per request, which the streaming segmenter gives you. Cartesia runs its own text normalizer (request field `normalization`, default `auto`); its docs recommend pre-normalizing and setting `normalization: off` when you need full control of the spoken form, which is exactly what this library's output is for. Pronunciations are managed server-side through `pronunciation_dict_id`, so there is no lexicon export.\n",
126
+ "sources": [
127
+ "https://docs.cartesia.ai/api-reference/tts/bytes",
128
+ "https://docs.cartesia.ai/build-with-cartesia/capability-guides/ssml-tags",
129
+ "https://docs.cartesia.ai/build-with-cartesia/capability-guides/text-normalization",
130
+ "https://docs.cartesia.ai/build-with-cartesia/tts-models/latest"
131
+ ],
132
+ "verified": "2026-10-29"
133
+ },
134
+ {
135
+ "id": "deepgram-aura",
136
+ "provider": "deepgram",
137
+ "models": [
138
+ "aura-2",
139
+ "aura"
140
+ ],
141
+ "default": true,
142
+ "syntax": "plain",
143
+ "escape": "none",
144
+ "audio_tags": false,
145
+ "limit": {
146
+ "unit": "chars",
147
+ "max": 2000
148
+ },
149
+ "intents": {
150
+ "pause": {
151
+ "render": "punctuation"
152
+ },
153
+ "spell": {
154
+ "render": "spaced"
155
+ },
156
+ "emphasis": {
157
+ "render": "{text}"
158
+ }
159
+ },
160
+ "lexicon": {
161
+ "format": "none"
162
+ },
163
+ "notes": "Plain text only: no SSML, no pause or pronunciation markup, prosody only through the `speed` query parameter. Aura-2 covers English, Spanish, German, French, Dutch, Italian and Japanese; no Turkish. The 2,000 character cap is the figure Deepgram's REST reference carried at the Aura launch; the current pages state no number and their chunking guide works in 100 to 200 character sentence pieces, so treat 2,000 as the ceiling and stream sentences in practice.\n",
164
+ "sources": [
165
+ "https://developers.deepgram.com/reference/text-to-speech/speak-request",
166
+ "https://developers.deepgram.com/docs/tts-models",
167
+ "https://developers.deepgram.com/docs/text-chunking-for-tts-optimization"
168
+ ],
169
+ "verified": "2026-10-29"
170
+ },
171
+ {
172
+ "id": "elevenlabs-v2",
173
+ "provider": "elevenlabs",
174
+ "models": [
175
+ "eleven_multilingual_v2",
176
+ "eleven_flash_v2_5",
177
+ "eleven_flash_v2",
178
+ "eleven_turbo_v2_5",
179
+ "eleven_turbo_v2",
180
+ "eleven_multilingual_v1",
181
+ "eleven_monolingual_v1"
182
+ ],
183
+ "default": true,
184
+ "syntax": "plain",
185
+ "escape": "none",
186
+ "audio_tags": false,
187
+ "limit": {
188
+ "unit": "chars",
189
+ "max": 10000,
190
+ "per_model": {
191
+ "eleven_flash_v2_5": 40000,
192
+ "eleven_flash_v2": 30000
193
+ }
194
+ },
195
+ "intents": {
196
+ "pause": {
197
+ "render": "<break time=\"{s}s\" />",
198
+ "min_ms": 100,
199
+ "max_ms": 3000
200
+ },
201
+ "spell": {
202
+ "render": "spaced"
203
+ },
204
+ "emphasis": {
205
+ "render": "{text}"
206
+ }
207
+ },
208
+ "lexicon": {
209
+ "format": "pls",
210
+ "alphabet": "ipa"
211
+ },
212
+ "notes": "Models before v3 accept the SSML break tag and nothing else from SSML. Pauses are capped at 3 seconds; the docs warn that many breaks in one request can destabilise output, so keep them few. Dashes and ellipses are the documented fallback. Character limits per model come from the models overview page; turbo models list none there, so they inherit the 10,000 default.\n",
213
+ "sources": [
214
+ "https://elevenlabs.io/docs/product/prompting/pauses",
215
+ "https://elevenlabs.io/docs/overview/models"
216
+ ],
217
+ "verified": "2026-10-27"
218
+ },
219
+ {
220
+ "id": "elevenlabs-v3",
221
+ "provider": "elevenlabs",
222
+ "models": [
223
+ "eleven_v3",
224
+ "eleven_v3_conversational",
225
+ "eleven_v4",
226
+ "eleven_v4_turbo"
227
+ ],
228
+ "default": false,
229
+ "syntax": "plain",
230
+ "escape": "none",
231
+ "audio_tags": true,
232
+ "limit": {
233
+ "unit": "chars",
234
+ "max": 5000,
235
+ "per_model": {
236
+ "eleven_v4": 10000
237
+ }
238
+ },
239
+ "intents": {
240
+ "pause": {
241
+ "render": "punctuation"
242
+ },
243
+ "spell": {
244
+ "render": "spaced"
245
+ },
246
+ "emphasis": {
247
+ "render": "{text}"
248
+ }
249
+ },
250
+ "lexicon": {
251
+ "format": "pls",
252
+ "alphabet": "ipa"
253
+ },
254
+ "notes": "v3 and v4 do not accept SSML break tags. Pacing comes from punctuation and audio tags such as [short pause]; unknown bracket tags made of words pass through for these models. Limits: eleven_v3 5,000 characters, eleven_v4 10,000; the conversational and turbo variants list none, so they take the 5,000 default until verified.\n",
255
+ "sources": [
256
+ "https://elevenlabs.io/docs/product/prompting/pauses",
257
+ "https://elevenlabs.io/docs/overview/models"
258
+ ],
259
+ "verified": "2026-10-27"
260
+ },
261
+ {
262
+ "id": "google-chirp3",
263
+ "provider": "google",
264
+ "models": [
265
+ "chirp3-hd",
266
+ "chirp-hd",
267
+ "gemini-tts"
268
+ ],
269
+ "default": true,
270
+ "syntax": "plain",
271
+ "escape": "none",
272
+ "audio_tags": false,
273
+ "limit": {
274
+ "unit": "bytes",
275
+ "max": 5000
276
+ },
277
+ "intents": {
278
+ "pause": {
279
+ "render": "punctuation"
280
+ },
281
+ "spell": {
282
+ "render": "spaced"
283
+ },
284
+ "emphasis": {
285
+ "render": "{text}"
286
+ }
287
+ },
288
+ "lexicon": {
289
+ "format": "google",
290
+ "alphabet": "ipa"
291
+ },
292
+ "notes": "Chirp 3 HD takes plain text in `input.text`; SSML on these voices is a preview that works only for synchronous requests, so this profile stays plain. Pauses can be steered with `[pause]`, `[pause short]` and `[pause long]`, but only in the `input.markup` field, never in `text`, so the profile renders pauses as punctuation and leaves markup to callers who use that field. Pronunciations go in the request's `custom_pronunciations` list (IPA or X-SAMPA), which is what the google lexicon export produces. The request body is capped at 5,000 bytes.\n",
293
+ "sources": [
294
+ "https://docs.cloud.google.com/text-to-speech/docs/chirp3-hd",
295
+ "https://docs.cloud.google.com/text-to-speech/quotas"
296
+ ],
297
+ "verified": "2026-10-29"
298
+ },
299
+ {
300
+ "id": "google-ssml",
301
+ "provider": "google",
302
+ "models": [
303
+ "neural2",
304
+ "wavenet",
305
+ "studio",
306
+ "standard",
307
+ "polyglot"
308
+ ],
309
+ "default": false,
310
+ "syntax": "ssml",
311
+ "escape": "xml",
312
+ "audio_tags": false,
313
+ "limit": {
314
+ "unit": "bytes",
315
+ "max": 5000
316
+ },
317
+ "intents": {
318
+ "pause": {
319
+ "render": "<break time=\"{ms}ms\"/>",
320
+ "min_ms": 0
321
+ },
322
+ "spell": {
323
+ "render": "<say-as interpret-as=\"characters\">{text}</say-as>"
324
+ },
325
+ "emphasis": {
326
+ "render": "<emphasis level=\"moderate\">{text}</emphasis>"
327
+ },
328
+ "phoneme": {
329
+ "render": "<phoneme alphabet=\"ipa\" ph=\"{ipa}\">{text}</phoneme>"
330
+ }
331
+ },
332
+ "lexicon": {
333
+ "format": "none"
334
+ },
335
+ "notes": "The classic voice families take full SSML in `input.ssml`. Output is the body; the caller wraps it in <speak>. Studio voices ignore <emphasis>, <mark>, <prosody pitch> and <lang>. Google documents no upper bound for <break time>. The 5,000-byte cap counts the SSML tags too. There is no lexicon upload for these voices, so lexicon entries are rendered inline as <phoneme> tags.\n",
336
+ "sources": [
337
+ "https://docs.cloud.google.com/text-to-speech/docs/ssml",
338
+ "https://docs.cloud.google.com/text-to-speech/docs/list-voices-and-types",
339
+ "https://docs.cloud.google.com/text-to-speech/quotas"
340
+ ],
341
+ "verified": "2026-10-29"
342
+ },
343
+ {
344
+ "id": "openai-tts",
345
+ "provider": "openai",
346
+ "models": [
347
+ "gpt-4o-mini-tts",
348
+ "gpt-4o-mini-tts-2025-12-15",
349
+ "tts-1",
350
+ "tts-1-hd"
351
+ ],
352
+ "default": true,
353
+ "syntax": "plain",
354
+ "escape": "none",
355
+ "audio_tags": false,
356
+ "limit": {
357
+ "unit": "chars",
358
+ "max": 4096
359
+ },
360
+ "intents": {
361
+ "pause": {
362
+ "render": "punctuation"
363
+ },
364
+ "spell": {
365
+ "render": "spaced"
366
+ },
367
+ "emphasis": {
368
+ "render": "{text}"
369
+ }
370
+ },
371
+ "lexicon": {
372
+ "format": "none"
373
+ },
374
+ "notes": "No SSML. Tone is steered with the separate `instructions` request parameter, which does not work with tts-1 or tts-1-hd. The input is capped at 4,096 characters. OpenAI has announced that these models shut down on 6 January 2027 in favour of the Realtime API, so this profile is maintained until then.\n",
375
+ "sources": [
376
+ "https://developers.openai.com/api/docs/api-reference/audio/createSpeech",
377
+ "https://developers.openai.com/api/docs/guides/text-to-speech"
378
+ ],
379
+ "verified": "2026-10-28"
380
+ },
381
+ {
382
+ "id": "plain",
383
+ "provider": "plain",
384
+ "models": [],
385
+ "default": true,
386
+ "syntax": "plain",
387
+ "escape": "none",
388
+ "audio_tags": false,
389
+ "limit": {
390
+ "unit": "chars",
391
+ "max": null
392
+ },
393
+ "intents": {
394
+ "pause": {
395
+ "render": "punctuation"
396
+ },
397
+ "spell": {
398
+ "render": "spaced"
399
+ },
400
+ "emphasis": {
401
+ "render": "{text}"
402
+ }
403
+ },
404
+ "lexicon": {
405
+ "format": "none"
406
+ },
407
+ "notes": "Provider-neutral output. Pauses become punctuation, spell-outs become spaced characters, emphasis is dropped. No request limit, so one chunk.\n",
408
+ "sources": [],
409
+ "verified": "2026-10-27"
410
+ }
411
+ ];
@@ -0,0 +1,35 @@
1
+ /** Provider profiles: data files that say what each TTS model accepts.
2
+ *
3
+ * The YAML files live in the repository's `spec/profiles`. The build step turns
4
+ * them into `data.ts` so the package needs no YAML parser at runtime. Each
5
+ * profile states the intent renderings, the request limit, the escaping rules
6
+ * and whether unknown bracket tags pass through.
7
+ */
8
+ export interface Profile {
9
+ id: string;
10
+ provider: string;
11
+ models: string[];
12
+ /** plain | ssml */
13
+ syntax: string;
14
+ /** none | xml */
15
+ escape: string;
16
+ /** chars | bytes */
17
+ limitUnit: string;
18
+ limitMax: number | null;
19
+ limitPerModel: Record<string, number>;
20
+ intents: Record<string, Record<string, unknown>>;
21
+ audioTags: boolean;
22
+ /** pls | google | none: how a lexicon is uploaded to this provider */
23
+ lexiconFormat: string;
24
+ lexiconAlphabet: string;
25
+ default: boolean;
26
+ notes: string;
27
+ sources: string[];
28
+ verified: string;
29
+ raw: Record<string, unknown>;
30
+ limitFor(model?: string | null): number | null;
31
+ }
32
+ export declare function listProfiles(): Profile[];
33
+ export declare function loadProfile(profileId: string): Profile;
34
+ /** Pick the profile for a provider and model. No provider = the `plain` profile. */
35
+ export declare function resolveProfile(provider?: string | null, model?: string | null): Profile;
@@ -0,0 +1,72 @@
1
+ /** Provider profiles: data files that say what each TTS model accepts.
2
+ *
3
+ * The YAML files live in the repository's `spec/profiles`. The build step turns
4
+ * them into `data.ts` so the package needs no YAML parser at runtime. Each
5
+ * profile states the intent renderings, the request limit, the escaping rules
6
+ * and whether unknown bracket tags pass through.
7
+ */
8
+ import { PROFILE_DATA } from "./data.js";
9
+ function fromDict(data) {
10
+ const limit = (data.limit ?? {});
11
+ const lexicon = (data.lexicon ?? {});
12
+ const profile = {
13
+ id: String(data.id),
14
+ provider: String(data.provider),
15
+ models: (data.models ?? []).map(String),
16
+ syntax: data.syntax ?? "plain",
17
+ escape: data.escape ?? "none",
18
+ limitUnit: limit.unit ?? "chars",
19
+ limitMax: limit.max ?? null,
20
+ limitPerModel: { ...(limit.per_model ?? {}) },
21
+ intents: { ...(data.intents ?? {}) },
22
+ audioTags: Boolean(data.audio_tags ?? false),
23
+ lexiconFormat: String(lexicon.format ?? "none"),
24
+ lexiconAlphabet: String(lexicon.alphabet ?? "ipa"),
25
+ default: Boolean(data.default ?? false),
26
+ notes: data.notes ?? "",
27
+ sources: (data.sources ?? []).map(String),
28
+ verified: String(data.verified ?? ""),
29
+ raw: data,
30
+ limitFor(model) {
31
+ if (model && model in this.limitPerModel)
32
+ return this.limitPerModel[model];
33
+ return this.limitMax;
34
+ },
35
+ };
36
+ return profile;
37
+ }
38
+ let cache = null;
39
+ function all() {
40
+ if (!cache)
41
+ cache = PROFILE_DATA.map(fromDict);
42
+ return cache;
43
+ }
44
+ export function listProfiles() {
45
+ return [...all()];
46
+ }
47
+ export function loadProfile(profileId) {
48
+ for (const p of all())
49
+ if (p.id === profileId)
50
+ return p;
51
+ throw new Error(`unknown profile ${JSON.stringify(profileId)}; known: ${all().map((p) => p.id).join(", ")}`);
52
+ }
53
+ /** Pick the profile for a provider and model. No provider = the `plain` profile. */
54
+ export function resolveProfile(provider, model) {
55
+ if (!provider && !model)
56
+ return loadProfile("plain");
57
+ const matches = all().filter((p) => !provider || p.provider === provider);
58
+ if (model) {
59
+ for (const p of matches)
60
+ if (p.models.includes(model))
61
+ return p;
62
+ if (!provider)
63
+ throw new Error(`no profile lists model ${JSON.stringify(model)}`);
64
+ }
65
+ for (const p of matches)
66
+ if (p.default)
67
+ return p;
68
+ if (matches.length)
69
+ return matches[0];
70
+ const known = [...new Set(all().map((p) => p.provider))].sort();
71
+ throw new Error(`no profile for provider ${JSON.stringify(provider)}; known providers: ${known.join(", ")}`);
72
+ }
package/dist/util.d.ts ADDED
@@ -0,0 +1,53 @@
1
+ /** Small helpers that make the TypeScript port line up with the Python reference.
2
+ *
3
+ * Python's `re` is Unicode-aware by default: `\w` matches letters in any script
4
+ * and `\b` is a boundary next to them. JavaScript's `\w` and `\b` are ASCII only,
5
+ * so every pattern in this package is written with `W` and `B` instead and
6
+ * compiled with the `u` flag.
7
+ */
8
+ /** Python's `\w` under Unicode: letters, digits, underscore. Use inside a character class. */
9
+ export declare const W = "\\p{L}\\p{N}_";
10
+ /** Python's `\b` under Unicode. */
11
+ export declare const B = "(?:(?<=[\\p{L}\\p{N}_])(?![\\p{L}\\p{N}_])|(?<![\\p{L}\\p{N}_])(?=[\\p{L}\\p{N}_]))";
12
+ /** Compile a pattern with the `u` flag (plus `g`, which every helper here expects). */
13
+ export declare function re(source: string, flags?: string): RegExp;
14
+ /** A match as the Python-style callbacks see it: `m[0]`, `m[1]`..., `m.index`, `m.groups`. */
15
+ export type Match = RegExpExecArray;
16
+ /** `pattern.sub(fn, text)`: every match replaced by `fn(match)`. */
17
+ export declare function sub(pattern: RegExp, text: string, fn: (m: Match) => string): string;
18
+ /** `pattern.sub(repl, text)` with a plain string replacement (no `$` expansion). */
19
+ export declare function subs(pattern: RegExp, text: string, repl: string): string;
20
+ /** First match anywhere, or null. Safe on global patterns. */
21
+ export declare function search(pattern: RegExp, text: string): Match | null;
22
+ /** `pattern.match(text)`: a match anchored at the start, or null. */
23
+ export declare function matchStart(pattern: RegExp, text: string): Match | null;
24
+ /** `re.fullmatch(pattern, text)`. */
25
+ export declare function fullmatch(pattern: RegExp, text: string): Match | null;
26
+ export declare function test(pattern: RegExp, text: string): boolean;
27
+ /** All matches, in order. */
28
+ export declare function finditer(pattern: RegExp, text: string): Match[];
29
+ export declare function findall(pattern: RegExp, text: string): string[];
30
+ /** `re.escape`: escape what is special in a pattern (letters are left alone, which the `u` flag requires). */
31
+ export declare function escapeRe(s: string): string;
32
+ export declare function group(m: Match, i: number): string | undefined;
33
+ export declare function strip(s: string, chars?: string): string;
34
+ export declare function lstrip(s: string, chars?: string): string;
35
+ export declare function rstrip(s: string, chars?: string): string;
36
+ export declare function partition(s: string, sep: string): [string, string, string];
37
+ export declare function rpartition(s: string, sep: string): [string, string, string];
38
+ export declare function isDigit(s: string): boolean;
39
+ export declare function isUpper(ch: string): boolean;
40
+ export declare function isAlpha(s: string): boolean;
41
+ export declare function isSpace(ch: string): boolean;
42
+ export declare function chars(s: string): string[];
43
+ /** Python's `round()`: ties go to the even neighbour. */
44
+ export declare function roundHalfEven(x: number): number;
45
+ /** Python's `divmod` for non-negative integers. */
46
+ export declare function divmod(n: number, d: number): [number, number];
47
+ /** `str.format` for the small templates in provider profiles: `{ms}`, `{s}`, `{text}`, `{ipa}`. */
48
+ export declare function format(template: string, values: Record<string, string | number>): string;
49
+ export declare function escapeXml(text: string): string;
50
+ /** `len(text.encode("utf-8"))`. */
51
+ export declare function utf8Length(text: string): number;
52
+ /** `len(text)` in code points, as Python counts. */
53
+ export declare function codePoints(text: string): number;