@pipedog/sentencex-wasm 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ BSD 2-Clause License
2
+
3
+ Copyright (c) 2024–2026, Daily
4
+ Copyright (c) 2026, pipedog contributors
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Santhosh Thottingal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/NOTICE ADDED
@@ -0,0 +1,9 @@
1
+ pipedog is an unofficial TypeScript port of Pipecat (https://github.com/pipecat-ai/pipecat),
2
+ a framework for voice and multimodal conversational AI created by Daily.
3
+
4
+ It is not affiliated with or endorsed by Daily. Pipecat is distributed under
5
+ the BSD 2-Clause License, Copyright (c) 2024–2026, Daily; that copyright notice
6
+ and license are retained in LICENSE, and every ported source file carries a
7
+ header naming the upstream file it was derived from.
8
+
9
+ The upstream commit each file was ported from is recorded in porting/manifest.json.
package/README.md CHANGED
@@ -1,3 +1,132 @@
1
- # Temporary Holding Version
1
+ # @pipedog/sentencex-wasm
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Sentence segmentation for pipedog with [sentencex](https://github.com/wikimedia/sentencex)
4
+ (Wikimedia, MIT), compiled to WebAssembly. Upstream Pipecat splits sentences with
5
+ `sentencex==1.0.32` (`pipecat/utils/string.py`). This package runs the same Rust core, so
6
+ pipedog gets the same sentence boundaries as upstream on every runtime.
7
+
8
+ - The `sentencex` npm package is a native Node addon with per-platform binaries. It cannot run
9
+ on Deno or Cloudflare Workers. This package can.
10
+ - The Rust crate is [`sentencex` 0.1.32](https://crates.io/crates/sentencex/0.1.32) from
11
+ crates.io, pinned with `=0.1.32`. It is the same release as the Python and npm `1.0.32`: git
12
+ tag `v1.0.32`, commit `268ff94` (the crate's `.cargo_vcs_info.json`). Its `src/` is
13
+ byte-identical to the tag. Nothing is vendored or patched. `rust/Cargo.lock` pins `regex`,
14
+ `regex-automata`, `regex-syntax`, `aho-corasick`, `memchr` and `rustc-hash` to the versions in
15
+ sentencex's own `Cargo.lock` at that tag.
16
+ - No wasm-bindgen and no generated JS glue. The module has **zero imports** and a small C ABI
17
+ (`rust/src/lib.rs`). `src/engine.ts` wraps the `WebAssembly.Instance` exports by hand.
18
+ - `wasm/sentencex.wasm` is a committed build artifact of **1,054,520 bytes** (353 KB gzipped).
19
+ It is built with cargo `opt-level = "z"`, LTO, `panic = "abort"`, then `wasm-opt -Oz`.
20
+ Most of the size comes from the `regex` crate, which sentencex depends on with default
21
+ features: about 350 KB of regex engine code and its Unicode tables. The language rules
22
+ themselves (abbreviation lists etc.) are a smaller part.
23
+ - The API is **synchronous**, including the first call. Verified on Bun, Node 24, Deno 2 and
24
+ workerd (Cloudflare Workers).
25
+ - Output is identical to the native `sentencex@1.0.32` addon on more than 67,000
26
+ (language, text) inputs (`test/parity.test.ts`).
27
+
28
+ ## Usage
29
+
30
+ ```ts
31
+ import { getSentenceBoundaries, segment } from "@pipedog/sentencex-wasm";
32
+
33
+ segment("en", "Dr. Smith arrived at 5 p.m. He was late.");
34
+ // ["Dr. Smith arrived at 5 p.m. ", "He was late."]
35
+
36
+ getSentenceBoundaries("en", "Hi 👋. Bye.");
37
+ // [
38
+ // { start_index: 0, end_index: 7, text: "Hi 👋. ", boundary_symbol: ".", is_paragraph_break: false },
39
+ // { start_index: 7, end_index: 11, text: "Bye.", boundary_symbol: ".", is_paragraph_break: false },
40
+ // ]
41
+ ```
42
+
43
+ - `segment(language, text): string[]` returns the same result as sentencex's `segment`.
44
+ Joining the result gives back `text`, including whitespace and `"\n\n"` paragraph spans.
45
+ - `getSentenceBoundaries(language, text): SentenceBoundary[]` returns the same shape as the npm
46
+ package's `get_sentence_boundaries` typings (`start_index`, `end_index`, `text`,
47
+ `boundary_symbol`, `is_paragraph_break`). `get_sentence_boundaries` is exported as an alias.
48
+ - **Indices are JavaScript string (UTF-16) indices**, so
49
+ `text.slice(b.start_index, b.end_index) === b.text`. sentencex itself returns Unicode scalar
50
+ (code point) indices. The npm and Python packages pass those through unchanged, so they do not
51
+ match JS string positions after an emoji or any other astral character. The conversion
52
+ happens in Rust (`Utf16Cursor`). The parity test converts the native indices the same way
53
+ before it compares them.
54
+ - `language` is a code such as `"en"`, `"de"` or `"ja"`. Codes that sentencex does not know go
55
+ through its fallback map and end at English, just as in the native package. Upstream
56
+ normalizes codes first (`resolve_sentence_tokenizer_language`: lowercase, strip the region).
57
+ That is core's job, not this package's.
58
+ - Lone surrogates: sentencex sees them as U+FFFD, as the native addon does, and the boundaries
59
+ are the same. The returned strings are slices of your input, so the original code units are
60
+ kept.
61
+ - `initSentencex(source?)` compiles and instantiates now instead of on the first call.
62
+ `source` is a `WebAssembly.Module` or wasm bytes (bytes do not work on workerd), and it
63
+ replaces the bundled module. `getSentencex()` returns the shared `SentencexEngine`.
64
+ `new SentencexEngine(module)` creates a separate instance.
65
+ - Cost: the first call takes about 10 to 12 ms (compiling the module and sentencex's regexes
66
+ for English). Later calls take about 7 to 10 µs for a short sentence (Bun, Node, Deno on
67
+ Apple Silicon). The first call for another language compiles that language's regexes, about
68
+ 1.5 ms.
69
+
70
+ ## Loading on each runtime
71
+
72
+ `package.json` `"exports"["."]` uses export conditions:
73
+
74
+ | condition | entry | how the module gets loaded |
75
+ |---|---|---|
76
+ | `workerd` | `src/workerd.ts` | `import wasm from "../wasm/sentencex.wasm"` gives a compiled `WebAssembly.Module` (wrangler's default `.wasm` rule). It is instantiated synchronously, with `new WebAssembly.Instance(module, {})`, when the entry module loads. workerd refuses to compile WASM from bytes. |
77
+ | `default` | `src/index.ts` | The bytes are embedded as base64 in the generated `src/wasm_bytes.ts`. On the first call they are decoded (`Uint8Array.fromBase64` when available, else `atob`) and compiled with `new WebAssembly.Module(bytes)`, then `new WebAssembly.Instance`. There is no file access and no `fetch`, so it works the same in bundles. |
78
+
79
+ Synchronous compilation of a module this size works on Bun, Node 24 and Deno 2; the compat
80
+ harness checks all three. Browsers can refuse to compile large modules synchronously on the
81
+ main thread. For a browser, first run `initSentencex(await WebAssembly.compile(bytes))` with the
82
+ bytes of `./sentencex.wasm`. Browsers are not tested.
83
+
84
+ The embedded base64 adds about 1.4 MB to the default entry's JavaScript. On workerd only the
85
+ `.wasm` module is bundled.
86
+
87
+ ## C ABI (for reference)
88
+
89
+ | export | |
90
+ |---|---|
91
+ | `sx_alloc(len) -> ptr`, `sx_free(ptr, len)` | raw linear-memory blocks |
92
+ | `sx_segment(lang, lang_len, text, text_len) -> n` | `n` records of `[start16, end16]` at `sx_out()` |
93
+ | `sx_boundaries(lang, lang_len, text, text_len) -> n` | `n` records of `[start_index16, end_index16, text_start16, text_end16, symbol_ptr, symbol_len, is_paragraph_break]`, where `symbol_len = 0xFFFFFFFF` means no symbol |
94
+ | `sx_out() -> *u32` | records from the last call |
95
+
96
+ All values are u32. A negative count means invalid UTF-8 input. The JS side writes
97
+ `language` and `text` into one allocation with `TextEncoder.encodeInto`. It reads the records,
98
+ slices the original string, decodes the boundary symbols, and only then frees the buffer,
99
+ because symbols point into it. If the module traps (a Rust panic), the engine re-instantiates
100
+ it on the next call.
101
+
102
+ ## Licensing
103
+
104
+ - Our code (`src/`, `rust/src/lib.rs`, scripts and tests) is BSD-2-Clause.
105
+ - `wasm/sentencex.wasm` and `src/wasm_bytes.ts` contain compiled sentencex (MIT, © 2025 Santhosh
106
+ Thottingal; see [`LICENSE-sentencex`](./LICENSE-sentencex)). They also contain its
107
+ dependencies `regex`, `regex-automata`, `regex-syntax` and `rustc-hash` (MIT OR Apache-2.0),
108
+ `aho-corasick` and `memchr` (Unlicense OR MIT), and Rust's `std` and `dlmalloc`
109
+ (MIT OR Apache-2.0). Each of these is available under MIT. That is why the package `license` is `BSD-2-Clause AND MIT`.
110
+
111
+ ## Building and testing
112
+
113
+ ```sh
114
+ rustup target add wasm32-unknown-unknown
115
+ bun run build:wasm # cargo + wasm-opt -Oz -> wasm/sentencex.wasm and src/wasm_bytes.ts; --no-opt to skip wasm-opt
116
+ bun test # unit tests + parity against the native sentencex@1.0.32 addon
117
+ bun scripts/extract-upstream-corpus.ts # refresh test/fixtures/upstream-corpus.json from .upstream
118
+ bun tools/compat/run.ts --package sentencex-wasm # from the repo root: bun, node, deno, workerd
119
+ ```
120
+
121
+ The parity test needs the native `sentencex` devDependency, which has prebuilt binaries for
122
+ macOS, Linux x64/arm64 and Windows x64. Its corpus covers:
123
+
124
+ - every prefix of a hand-written corpus: abbreviations, initials, times, decimals, ellipses,
125
+ quotes, attributed questions and exclamations, URLs, emails, lists, paragraph breaks, emoji
126
+ and other astral characters, in en, es, fr, de, ja, zh, hi, ar, pt, pl, it and nl
127
+ - every string literal in upstream's `test_english_sentence_aggregation.py`,
128
+ `test_sentence_tokenizer_language.py`, `test_sentencex_tokenizer.py`,
129
+ `test_simple_text_aggregator.py` and `test_utils_string.py`, alone and joined with its
130
+ neighbour, in 12 languages and as streaming prefixes
131
+ - a text over 10 KB, which takes sentencex's paragraph-chunking path
132
+ - a mixed sample in every language code sentencex knows, plus unknown codes