speechready 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/README.md CHANGED
@@ -57,6 +57,17 @@ for (const token of llmStream) {
57
57
  for (const sentence of seg.flush()) speak(sentence);
58
58
  ```
59
59
 
60
+ For LiveKit Agents (Node), the same thing as a `ttsTextTransforms` entry; sentences are released as they are confirmed, and a fenced code block is handled as one piece:
61
+
62
+ ```js
63
+ import { textTransform } from "speechready/livekit";
64
+
65
+ const session = new voice.AgentSession({
66
+ ...,
67
+ ttsTextTransforms: [textTransform({ provider: "elevenlabs", model: "eleven_turbo_v2_5" })],
68
+ });
69
+ ```
70
+
60
71
  Pronunciation lexicon, one file for every vendor:
61
72
 
62
73
  ```js
package/dist/chunk.d.ts CHANGED
@@ -11,16 +11,12 @@
11
11
  */
12
12
  /** Split text into sentences. Newlines are hard boundaries. Empty sentences are dropped. */
13
13
  export declare function splitSentences(text: string, locale?: string): string[];
14
- /** Streaming sentence segmenter for LLM token feeds.
15
- *
16
- * `push(text)` returns the sentences that are now complete; `flush()` returns
17
- * whatever is left. A boundary is only confirmed once text after it arrives,
18
- * so a sentence is never cut at "Dr." or "3.14" because the stream paused there.
19
- */
20
14
  export declare class Segmenter {
21
15
  locale: string;
22
16
  maxBuffer: number | null;
23
17
  private buffer;
18
+ /** Lines of an open fenced code block; released whole once the fence closes. */
19
+ private fence;
24
20
  constructor(init?: {
25
21
  locale?: string;
26
22
  maxBuffer?: number | null;
package/dist/chunk.js CHANGED
@@ -93,10 +93,13 @@ export function splitSentences(text, locale = "en-US") {
93
93
  * whatever is left. A boundary is only confirmed once text after it arrives,
94
94
  * so a sentence is never cut at "Dr." or "3.14" because the stream paused there.
95
95
  */
96
+ const FENCE = /^\s*(```|~~~)/u;
96
97
  export class Segmenter {
97
98
  locale;
98
99
  maxBuffer;
99
100
  buffer = "";
101
+ /** Lines of an open fenced code block; released whole once the fence closes. */
102
+ fence = null;
100
103
  constructor(init = {}) {
101
104
  this.locale = init.locale ?? "en-US";
102
105
  this.maxBuffer = init.maxBuffer ?? null;
@@ -110,8 +113,22 @@ export class Segmenter {
110
113
  break;
111
114
  const head = this.buffer.slice(0, nl);
112
115
  this.buffer = this.buffer.slice(nl + 1);
113
- done.push(...splitSentences(head, this.locale));
116
+ if (this.fence !== null) {
117
+ this.fence.push(head);
118
+ if (FENCE.test(head)) {
119
+ done.push(this.fence.join("\n"));
120
+ this.fence = null;
121
+ }
122
+ }
123
+ else if (FENCE.test(head)) {
124
+ this.fence = [head];
125
+ }
126
+ else {
127
+ done.push(...splitSentences(head, this.locale));
128
+ }
114
129
  }
130
+ if (this.fence !== null)
131
+ return done.filter((s) => s); // inside a fence: nothing is sentence-split
115
132
  const sentences = splitSentences(this.buffer, this.locale);
116
133
  if (sentences.length > 1) {
117
134
  done.push(...sentences.slice(0, -1));
@@ -131,6 +148,11 @@ export class Segmenter {
131
148
  flush() {
132
149
  const rest = this.buffer;
133
150
  this.buffer = "";
151
+ if (this.fence !== null) {
152
+ const block = [...this.fence, ...(rest ? [rest] : [])].join("\n");
153
+ this.fence = null;
154
+ return [block];
155
+ }
134
156
  return splitSentences(rest, this.locale);
135
157
  }
136
158
  }
@@ -0,0 +1,19 @@
1
+ /** speechready as a LiveKit Agents text transform.
2
+ *
3
+ * LiveKit Agents for Node passes the LLM's text stream through `ttsTextTransforms`
4
+ * before synthesis. Each transform is `(AsyncIterable<string>) => AsyncIterable<string>`.
5
+ * This one segments the stream into sentences as they are confirmed, runs each
6
+ * through `prepare`, and yields the result.
7
+ *
8
+ * import { textTransform } from "speechready/livekit";
9
+ * const session = new voice.AgentSession({
10
+ * ...,
11
+ * ttsTextTransforms: [textTransform({ provider: "elevenlabs", model: "eleven_turbo_v2_5" })],
12
+ * });
13
+ */
14
+ import type { PrepareInit } from "../pipeline.js";
15
+ export interface TextTransformInit extends Omit<PrepareInit, "limit"> {
16
+ /** Release a long sentence at a clause boundary once the buffer holds this many characters. */
17
+ maxBuffer?: number | null;
18
+ }
19
+ export declare function textTransform(init?: TextTransformInit): (text: AsyncIterable<string>) => AsyncIterable<string>;
@@ -0,0 +1,35 @@
1
+ /** speechready as a LiveKit Agents text transform.
2
+ *
3
+ * LiveKit Agents for Node passes the LLM's text stream through `ttsTextTransforms`
4
+ * before synthesis. Each transform is `(AsyncIterable<string>) => AsyncIterable<string>`.
5
+ * This one segments the stream into sentences as they are confirmed, runs each
6
+ * through `prepare`, and yields the result.
7
+ *
8
+ * import { textTransform } from "speechready/livekit";
9
+ * const session = new voice.AgentSession({
10
+ * ...,
11
+ * ttsTextTransforms: [textTransform({ provider: "elevenlabs", model: "eleven_turbo_v2_5" })],
12
+ * });
13
+ */
14
+ import { Segmenter } from "../chunk.js";
15
+ import { prepare } from "../pipeline.js";
16
+ export function textTransform(init = {}) {
17
+ const { maxBuffer = null, ...prepareInit } = init;
18
+ const locale = prepareInit.locale ?? "en-US";
19
+ const spoken = (piece) => prepare(piece, prepareInit).text;
20
+ return async function* transform(text) {
21
+ const seg = new Segmenter({ locale, maxBuffer });
22
+ for await (const chunk of text) {
23
+ for (const piece of seg.push(chunk)) {
24
+ const out = spoken(piece);
25
+ if (out)
26
+ yield out + " ";
27
+ }
28
+ }
29
+ for (const piece of seg.flush()) {
30
+ const out = spoken(piece);
31
+ if (out)
32
+ yield out + " ";
33
+ }
34
+ };
35
+ }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const version = "0.1.0";
1
+ export declare const version = "0.2.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by scripts/build-profiles.mjs from package.json. Do not edit.
2
- export const version = "0.1.0";
2
+ export const version = "0.2.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "speechready",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Prepare text for text-to-speech: clean LLM output, verbalize ambiguous tokens, compile pauses to each provider's syntax, chunk under model limits.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -9,18 +9,38 @@
9
9
  ".": {
10
10
  "types": "./dist/index.d.ts",
11
11
  "import": "./dist/index.js"
12
+ },
13
+ "./livekit": {
14
+ "types": "./dist/integrations/livekit.d.ts",
15
+ "import": "./dist/integrations/livekit.js"
12
16
  }
13
17
  },
14
18
  "license": "MIT",
15
19
  "author": "Berker Batur",
16
- "repository": { "type": "git", "url": "https://github.com/berkerbatur/speechready", "directory": "js" },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/berkerbatur/speechready.git",
23
+ "directory": "js"
24
+ },
17
25
  "homepage": "https://github.com/berkerbatur/speechready",
18
- "keywords": ["tts", "text-to-speech", "text-normalization", "ssml", "voice-agents"],
19
- "files": ["dist", "README.md", "LICENSE"],
20
- "engines": { "node": ">=20" },
26
+ "keywords": [
27
+ "tts",
28
+ "text-to-speech",
29
+ "text-normalization",
30
+ "ssml",
31
+ "voice-agents"
32
+ ],
33
+ "files": [
34
+ "dist",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "engines": {
39
+ "node": ">=20"
40
+ },
21
41
  "scripts": {
22
42
  "build": "node scripts/build-profiles.mjs && tsc -p tsconfig.json",
23
- "test": "npm run build && node --test test/spec.test.mjs test/package.test.mjs",
43
+ "test": "npm run build && node --test test/spec.test.mjs test/package.test.mjs test/livekit.test.mjs",
24
44
  "prepack": "npm run build"
25
45
  },
26
46
  "devDependencies": {