sentencex

Library Listed

Sentence segmentation library with wide language support optimized for speed and utility.

Checked · Sources

Downloads
114,914
Dependents
1
Stars
145
Forks
18
Contributors
14
Commits
382
Latest release
0.1.34 ·

About

From the project README ·

Sentence segmenter

A sentence segmentation library written in Rust language with wide language support optimized for speed and utility.

Bindings

Besides native Rust, bindings for the following programming languages are available:

Read the full README

Approach

  • A sentence-terminating punctuation mark (., ?, !, plus language specific terminators like । or ။) ends a sentence by default. Some languages use different terminators so a list of known terminator symbols is maintained for supported languages.
  • Hand-compiled abbreviation lists, exclamation words, numbered references (See [1]. Next sentence.), and quote-aware rules (see below) suppress or relocate boundaries where the default rule would over-split. We collect a list of known, popular abbreviations in supported languages.
  • List-item starts (e.g., bullets * / + / - / •, numeric 1. / 1) / (1), lettered a) / (a), roman ii.) emit sentence boundaries so each item segments cleanly, even when items are written inline on one line. A sibling rule (≥2 matches of the same marker family per paragraph, or a single Tier-1 line-start) keeps prose with stray (1894) or e. e. cummings from being mis-split.
  • Multi-character punctuation runs (. . ., ! ?, ? ? ?, glued or space-separated) are treated as a single terminator. This generalises the ellipsis (… / ...) case: any mix of ., !, ? - repeated, spaced, or interleaved - collapses into one boundary candidate instead of several. Continuation heuristics then decide whether the following token starts a new sentence: uppercase non-I splits, while lowercase, digits, or glued continuations (e.g. mean...see) keep the sentence intact.
  • Starter-word overrides recover boundaries that abbreviation and name-initial rules would otherwise suppress. When a suppressed terminator is followed by a known sentence-starter word, the break is reinstated. Languages opt in by overriding a trait method and shipping a starter-word list; English is currently the only language with one.
  • Trailing-marker rules add context-aware handling for select abbreviations that usually trail a number or name instead of ending a sentence, e.g., time-of-day (a.m. / p.m.), eras (B.C. / A.D. / C.E.), imperial units (5 ft., 60 mph), academic titles (Ph.D., M.D.), and compass bearings (NE.). Each marker carries a policy for whether a following digit or capitalised non-starter word reopens the boundary, and a curated "fronting-word" list recognises adverbial lead-ins (In the evening at 7 p.m.) so a framing phrase before the marker doesn't trigger a false split. English is currently the only language shipping marker and fronting lists.

Sometimes, it is very hard to get the segmentation correct. In such cases this library is opinionated and prefer not segmenting than wrong segmentation. If two sentences are accidentally together, that is ok. It is better than sentence being split in middle. Avoid over engineering to get everything linguistically 100% accurate.

This approach would be suitable for applications like text to speech, machine translation.

Trade-offs

The opinionated don't-over-split stance coexists with several rules that do recover real boundaries (numbered references, quote-aware handling, abbreviation/exclamation suppression). The aim is to be conservative where context is ambiguous, while still picking up structural signals that make a split safe.

Consider this example: We make a good team, you and I. Did you see Albert I. Jones yesterday?

The accurate splitting is ["We make a good team, you and I.", "Did you see Albert I. Jones yesterday?"]

The hard part is that the same I. shape appears twice in the sentence and has to be read differently each time. The trailing I. of the first clause is a real sentence terminator; the I. in Albert I. Jones is a name initial and the boundary there must stay suppressed. Structurally the two tokens are indistinguishable - telling them apart reliably needs semantic understanding of the surrounding noun phrase, which is outside the scope of a rule-based segmenter and fits ML approaches better. So by default the name-initial detector takes the conservative line and suppresses both, leaving the two sentences joined. For most downstream applications that is fine.

The starter-word override is a narrow carve-out to that posture. When a suppressed terminator is followed by a token that appears in a small, curated list of known sentence-starters, the boundary is restored. Did is on the English starter list, so it splits at that point. Jones is not, hence Albert I. Jones stays joined. The match is case-sensitive on the following token. A lowercase variant like …you and I. did you see… is still left joined - a lowercase opener is a weaker signal and the conservative default applies. On the flip side, common words that are also abbreviations like man, mass, wash, are omitted from the abbreviation list as they collide too often. Starter words do not provide a strong enough signal in that situation, so the conservative posture applies in the other direction. Therefore an example like Even the most brilliant strategy can be derailed by the unpredictable nature of man. Mistakes happen. will still split as ["Even the most brilliant strategy can be derailed by the unpredictable nature of man. ", "Mistakes happen."]

The same conservative posture drives trailing-marker handling. An abbreviation like p.m. is ambiguous: it can sit mid-sentence (In the evening at 7 p.m. Tom wakes up.) or end one (The sun sets at 7 p.m. Tom wakes up then.). Rather than always suppressing or always splitting, each marker carries a policy keyed on what follows it, e.g., a digit or a capitalised word that isn't a known starter word. A small curated "fronting-word" list supplies the disambiguating context. When every word before the marker is an adverbial lead-in (In the evening at), the phrase reads as framing and the boundary stays suppressed. Otherwise a capitalised follower like Tom reopens it. The sun sets at 7 p.m. Tom wakes up then. splits into ["The sun sets at 7 p.m. ", "Tom wakes up then."], while In the evening at 7 p.m. Tom wakes up. stays joined.

List-item detection follows the same conservative posture. Ambiguous inline shapes that collide with prose (bare 1. / a. / ii. closers inline, single-letter a. patterns that look like initials, parenthesised numbers like (1894) that read as years) deliberately do not trigger list segmentation. A sibling rule (≥2 matches of the same marker family per paragraph, or a single Tier-1 line-start) further suppresses one-off occurrences. The result: real lists segment per item, but prose containing list-shaped fragments stays intact.

Several other small heuristics follow the same "recover when the signal is clear, otherwise leave it joined" posture:

  • Stray punctuation around terminators: a period immediately followed by a comma (…ice cream. , It was…) is treated as stray punctuation and the sentence continues through it. Whitespace between an abbreviation and its terminator (U.S .) is tolerated when looking up the abbreviation, so the boundary is still suppressed.
  • Dot-coded tokens like chess notation: tokens of the shape <digit>.<letter…> (e.g. 7.Bg5, 1.e4) do not emit a boundary, so move codes and similar dot-coded identifiers stay inside their sentence.
  • Slash-joined abbreviations: tokens like 171/U.S. are split on / when extracting the trailing word, so the abbreviation on the right-hand side is still recognised.

The sentence segmentation in this library is non-destructive. This means, if the sentences are combined together, you can reconstruct the original text. Line breaks, punctuations and whitespaces are preserved in the output.

Usage

Rust

Install the library using

cargo add sentencex

Then, any text can be segmented as follows.

use sentencex::segment;

fn main() {
    let text = "The James Webb Space Telescope (JWST) is a space telescope specifically designed to conduct infrared astronomy. The U.S. National Aeronautics and Space Administration (NASA) led Webb's design and development.";
    let sentences = segment("en", text);

    for (i, sentence) in sentences.iter().enumerate() {
        println!("{}. {}", i + 1, sentence);
    }
}

The first argument is language code, second argument is text to segment. The segment method returns an array of identified sentences.

Python

Install from PyPI:

pip install sentencex
import sentencex

text = "The James Webb Space Telescope (JWST) is a space telescope specifically designed to conduct infrared astronomy. The U.S. National Aeronautics and Space Administration (NASA) led Webb's design and development."

# Segment text into sentences
sentences = sentencex.segment("en", text)
for i, sentence in enumerate(sentences, 1):
    print(f"{i}. {sentence}")

# Get sentence boundaries with indices
boundaries = sentencex.get_sentence_boundaries("en", text)
for boundary in boundaries:
    print(f"Sentence: '{boundary['text']}' (indices: {boundary['start_index']}-{boundary['end_index']})")

See bindings/python/example.py for more examples.

Node.js

Install from npm:

npm install sentencex
import { segment, get_sentence_boundaries } from 'sentencex';

const text = "The James Webb Space Telescope (JWST) is a space telescope specifically designed to conduct infrared astronomy. The U.S. National Aeronautics and Space Administration (NASA) led Webb's design and development.";

// Segment text into sentences
const sentences = segment("en", text);
sentences.forEach((sentence, i) => {
    console.log(`${i + 1}. ${sentence}`);
});

// Get sentence boundaries with indices
const boundaries = get_sentence_boundaries("en", text);
boundaries.forEach(boundary => {
    console.log(`Sentence: '${boundary.text}' (indices: ${boundary.start_index}-${boundary.end_index})`);
});

For CommonJS usage:

const { segment, get_sentence_boundaries } = require('sentencex');

See bindings/nodejs/example.js for more examples.

WebAssembly (Browser)

Install from npm:

npm install sentencex-wasm

or use a CDN like https://esm.sh/sentencex-wasm

import init, { segment, get_sentence_boundaries } from 'https://esm.sh/sentencex-wasm;

async function main() {
    // Initialize the WASM module
    await init();

    const text = "The James Webb Space Telescope (JWST) is a space telescope specifically designed to conduct infrared astronomy. The U.S. National Aeronautics and Space Administration (NASA) led Webb's design and development.";

    // Segment text into sentences
    const sentences = segment("en", text);
    sentences.forEach((sentence, i) => {
        console.log(`${i + 1}. ${sentence}`);
    });

    // Get sentence boundaries with indices
    const boundaries = get_sentence_boundaries("en", text);
    boundaries.forEach(boundary => {
        console.log(`Sentence: '${boundary.text}' (indices: ${boundary.start_index}-${boundary.end_index})`);
    });
}

main();
C# / .NET 10+

Install from nuget:

dotnet add package sentencex
using Sentencex;

string language = "en";
string inputText = "The James Webb Space Telescope (JWST) is a space telescope specifically designed to conduct infrared astronomy. The U.S. National Aeronautics and Space Administration (NASA) led Webb's design and development.";
string[] sentences = Segmenter.Segment("en", inputText);

// Segment text into sentences
foreach (string sentence in sentences)
    Console.WriteLine($"Sentence: {sentence}");

// Get sentence boundaries with indices and text
SentenceBoundary[] boundaries = Segmenter.GetSentenceBoundaries(language, inputText);
foreach (SentenceBoundary boundary in boundaries)
    Console.WriteLine($"Sentence: '{boundary.Text}' (indices: {boundary.StartIndex}-{boundary.EndIndex})");

// Get sentence boundary indices without text
SentenceBoundarySlim[] boundariesSlim = Segmenter.GetSentenceBoundariesSlim(language, inputText);
foreach (SentenceBoundarySlim boundary in boundariesSlim)
    Console.WriteLine($"Sentence indices: {boundary.StartIndex}-{boundary.EndIndex}");

Language support

The aim is to support all languages where there is a wikipedia. Instead of falling back on English for languages not defined in the library, a fallback chain is used. The closest language which is defined in the library will be used. Fallbacks for ~244 languages are defined.

Performance

Following is a sample output of sentence segmenting The Complete Works of William Shakespeare. This file is 5.29MB. As you can see below, it took 52 milli second.

$ curl https://www.gutenberg.org/files/100/100-0.txt | ./target/release/sentencex -l en > /dev/null
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 5295k  100 5295k    0     0   630k      0  0:00:08  0:00:08 --:--:-- 1061k
Time taken for segment(): 52.09561ms
Total sentences: 150671

Measured on English Golden Rule Set (GRS) using mean F1 score across 60 test cases. The benchmark script is at benchmarks/compare.py and can be run with uv run benchmarks/compare.py.

The following libraries are compared:

  • mwtokenizer — Wikimedia rule-based tokenizer
  • blingfire — Microsoft's fast tokenizer (C library)
  • nltk — Punkt sentence tokenizer
  • pysbd — Python port of pragmatic segmenter
  • spacy — dependency-parse based sentence segmentation
  • syntok — rule-based tokenizer
TokenizerEnglish GRS F1 Score
sentencex100.00
pysbd93.00
blingfire91.67
syntok85.67
spacy81.67
mwtokenizer78.00
nltk72.33

Thanks

License

MIT license. See License.txt

Runs on
GPU support unknown
What shows it
  • no dependency or feature matches a backend row
Native code
None found
  • No native code found
Python bindings
No Python binding crate found
License
MIT

Compare with similar projects

Order: strongest tier first, then most downloads, then name, with unmeasured values last. A library counts downloads over 30 days. An app counts binary downloads per release. An app with none counts its crate downloads over 30 days. ML Rust runs this catalogue, so its closest project always gets a row.

sentencex beside the projects that share a topic with it
ProjectDownloadsDependentsStarsLatest releaseRuns on (GPU)Native codeLicense
sentencex This page Listed

Sentence segmentation library with wide language support optimized for speed and utility.

114,914 downloads over 30 days 1 145 0.1.34 GPU support unknown No native code found MIT
tiktoken-rs Featured

Library for encoding and decoding with the tiktoken library in Rust

3,939,182 downloads over 30 days 449 410 0.12.1 GPU support unknown No native code found MIT
whatlang-rs Featured

Fast and lightweight language identification library for Rust.

882,019 downloads over 30 days 74 1,087 0.18.0 GPU support unknown No native code found MIT
lingua-rs Featured

An accurate natural language detection library, suitable for short text and mixed-language text

Python bindings

286,625 downloads over 30 days 22 1,152 1.8.0 GPU support unknown Binds native code: cld2-sys (feature accuracy-reports) Apache-2.0
whichlang Listed

A blazingly fast and lightweight language detection library for Rust.

52,880 downloads over 30 days 11 464 0.1.1 GPU support unknown No native code found MIT
splintr Listed

Tokenizer for Rust and Python: pure Rust by default, with 18 bundled vocabularies and four backends covering BPE, SentencePiece and WordPiece. Its output matches Hugging Face tokenizers id for id on 14 model families, including GPT-2, Qwen, Whisper, T5 and BERT.

Python bindings

8,204 downloads over 30 days 7 63 0.22.2 GPU support unknown Binds native code: pcre2-sys (feature pcre2) MIT
xberg New

High-performance document intelligence library for Rust. Extract text, metadata, and structured data from PDFs, Office documents, images, and 107 formats and 371 programming languages via tree-sitter code intelligence with async/sync APIs.

26,095 downloads over 30 days 2 9,565 1.3.10 CUDA, Metal Binds native code: libheif-sys, libz-sys, xberg_libwpd, xberg_tesseract, clipper-sys and 4 more. Build uses a native toolchain: cc, bindgen (feature bindings), cmake (feature build-tesseract), pkg-config (feature dynamic-linking), vcpkg (feature dynamic-linking) MIT
Why ListedNo archive, New or Featured rule places it, so it is Listed.

What it lacks for Featured

  • It has 1 active dependents and 1 new dependents in 365 days. Featured needs 11 active dependents or 9 new dependents in 365 days. The repository route reads 145 stars, 18 forks and 14 contributors. It needs 4,539 stars, 434 forks and 79 contributors together.

What else the check found

  • The last activity is on 5 Nov 2026, 4 days ago. The rules expect an update within 900 days. Dormancy rule

Exact rules: Listed, Featured library. The check on applied thresholds version 9.

Every rule outcome, as the check records it
  • expected-interval: version 0.1.34 base 450, doubled for 114914 downloads per month: E = 900 days
  • dormancy: last activity 2026-11-05, 4 days ago, E = 900: active
  • featured: not featured: active_users 1 < 11 and new_adopters 1 < 9 and repository route stars 145 (needs 4539), forks 18 (needs 434), contributors 14 (needs 79)
  • listed: listed: no archive, new or featured rule applies

Measurements and sources

Each signal row shows the raw value, its source and the fetch date. The Sources list gathers every distinct source on this page. Movement needs more history.

Sources6 sources
Adoption1 active dependents, 1 new dependents in 365 days1 not measured
Adoption measurements
SignalValueSourceFetchedCounts
Active dependents1crates.io database dumpYes
New dependents (365 days)1crates.io database dumpYes
Peak active dependents1crates.io database dumpYes
Active dependents as a share of the peak1crates.io database dumpYes
Binary downloads per releaseNot measuredYes
MaintenanceLatest human commit: 4 days ago3 not measured
Maintenance measurements
SignalValueSourceFetchedCounts
Latest human commit2026-11-05GitHub repositoryYes
Latest release2026-11-05crates.io database dumpYes
First release2025-10-23crates.io database dumpYes
Latest version0.1.34crates.io database dumpYes
Days the oldest outside issue waits for a reply1100GitHub repositoryYes
Maintainer replies over 365 days32GitHub repositoryYes
Repository archivedfalseGitHub repositoryYes
Repository gonefalseGitHub repositoryYes
Release commit or manifest in the repotruecrates.io packageYes
Published docstruecrates.io database dumpYes
Latest release fails cargo checkNot measuredYes
Build check dateNot measuredYes
Build check toolchainNot measuredYes
SecurityUnpatched vulnerabilities: none
Security measurements
SignalValueSourceFetchedCounts
Unpatched vulnerabilities past the grace windownoneRustSec advisory databaseYes
RustSec unmaintained advisorynoneRustSec advisory databaseYes
Deprecation noticenonecrates.io database dumpYes
Deprecated requirementnonecrates.io database dumpYes
Distribution27 releases, 114,914 downloads over 30 days5 not measured
Distribution measurements
SignalValueSourceFetchedCounts
Releases27crates.io database dumpYes
Downloads over 30 days114914crates.io database dumpYes
Binary downloads over the last 3 releasesNot measuredYes
Homebrew installs on request over 365 daysNot measuredYes
Repology repository familiesNot measuredYes
Docker pull growth over 90 daysNot measuredYes
Docker pulls at the growth baselineNot measuredYes
Every version a prereleasefalsecrates.io database dumpYes
Every version 0.0.xfalsecrates.io database dumpYes
Every version yankedfalsecrates.io database dumpYes
Crate deleted from crates.iofalsecrates.io database dumpYes
Repository145 stars, 18 forks, 14 contributors, 382 commits. Stars alone never qualify a project.
Repository measurements
SignalValueSourceFetchedCounts
GitHub stars145GitHub repositoryYes — one part of the repository route
GitHub forks18GitHub repositoryYes — one part of the repository route
Contributors with a GitHub account14GitHub contributorsYes — one part of the repository route
Commits on the default branch382GitHub repositoryNo
Repository's main crate left out of the entrynonecrates.io database dumpYes
Facts that never count
Facts that never count measurements
SignalValueSourceFetchedCounts
LicenseMITcrates.io database dumpNo
Runs on, from listed crates and their direct dependenciesGPU support unknowncrates.io database dumpNo
What shows each backendno dependency or feature matches a backend rowcrates.io database dumpNo
Native code, from listed crates and their dependenciesNo native code foundcrates.io database dumpNo
Python bindings, from direct dependencies of listed cratesNo Python binding crate foundcrates.io database dumpNo
Crates that depend on a listed crate directly1crates.io database dumpNo

Corrections and review history

Report a correction

No correction applies to this entry.

Judgments

A judgment enters only through an approved issue and a merged pull request.

No judgment applies to this entry.