Repository: PyThaiNLP/nlpo3 (language: python)

Issue:
Add tool poetry
Add tool poetry for pypi


Discussion / hints:

Fix patch:
diff --git a/.cargo/config b/.cargo/config
new file mode 100644
index 0000000..d47f983
--- /dev/null
+++ b/.cargo/config
@@ -0,0 +1,11 @@
+[target.x86_64-apple-darwin]
+rustflags = [
+  "-C", "link-arg=-undefined",
+  "-C", "link-arg=dynamic_lookup",
+]
+
+[target.aarch64-apple-darwin]
+rustflags = [
+  "-C", "link-arg=-undefined",
+  "-C", "link-arg=dynamic_lookup",
+]
diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml
index b8c4474..8ef77da 100644
--- a/.github/workflows/wheels.yml
+++ b/.github/workflows/wheels.yml
@@ -1,49 +1,202 @@
 # Build wheels for many platforms
 # see: https://github.com/pypa/cibuildwheel
 
-name: Build
+name: Build Python wheels
 
-on: [push, pull_request]
+on:
+  push:
+    branches:
+      - main
+  pull_request:
+    branches:
+      - main
+  # Manual run
+  workflow_dispatch:
 
 jobs:
+  # Check whether to build the wheels and the source tarball
+  check_build_trigger:
+    name: Check build trigger
+    runs-on: ubuntu-latest
+    if: github.repository == 'pythainlp/oxidized-thainlp'
+    outputs:
+      build: ${{ steps.check_build_trigger.outputs.build }}
+
+    steps:
+      - name: Checkout source code
+        uses: actions/checkout@v2
+        with:
+          ref: ${{ github.event.pull_request.head.sha }}
+
+      - id: check_build_trigger
+        name: Check build trigger
+        run: bash build_tools/github/check_build_trigger.sh
+
   build_wheels:
-    name: Build wheels on ${{ matrix.os }}
+    name: Build Python wheel for ${{ matrix.python }}-${{ matrix.platform_id }}-${{ matrix.manylinux_image }}
     runs-on: ${{ matrix.os }}
-    
+    needs: check_build_trigger
+    if: needs.check_build_trigger.outputs.build
+
     strategy:
       # Ensure that a wheel builder finishes even if another fails
       fail-fast: false
       matrix:
-        os: [windows-latest, ubuntu-latest, macos-latest]
+        os: [macos-latest, ubuntu-latest, windows-latest]
+        python: [cp36, cp37, cp38, cp39]  # pp37
+        bitness: [32, 64]
+        manylinux_image: [manylinux2014]
+        include:
+          - os: macos-latest
+            bitness: 64
+            platform_id: macosx_x86_64
+          - os: ubuntu-latest
+            bitness: 64
+            platform_id: manylinux_x86_64
+          - os: ubuntu-latest
+            bitness: 32
+            platform_id: manylinux_i686
+          - os: windows-latest
+            bitness: 64
+            platform_id: win_amd64
+          - os: windows-latest
+            bitness: 32
+            platform_id: win32
+          - os: macos-latest
+            pip-path: ~/Library/Caches/pip
+          - os: ubuntu-latest
+            pip-path: ~/.cache/pip
+          - os: windows-latest
+            pip-path: ~\AppData\Local\pip\Cache
+        exclude:
+          - os: macos-latest
+            bitness: 32
+#          - python: pp37
+#            bitness: 32
 
     steps:
-      - uses: actions/checkout@v2
-
-      # Used to host cibuildwheel
-      - uses: actions/setup-python@v2
+      - name: Checkout source code
+        uses: actions/checkout@v2
+          
+      - name: Setup Rust toolchain - non-win32
+        uses: actions-rs/toolchain@v1
+        with:
+          override: true
+          profile: minimal
+          toolchain: stable
+        if: ${{ !startsWith(matrix.os, 'windows') || matrix.bitness != '32' }}
 
-      - uses: actions-rs/toolchain@v1
+      - name: Setup Rust toolchain - win32
+        uses: actions-rs/toolchain@v1
         with:
+          override: true
           profile: minimal
           toolchain: stable
+          target: i686-pc-windows-msvc
+        if: startsWith(matrix.os, 'windows') && matrix.bitness == '32'
 
-      - uses: actions-rs/cargo@v1
+      - name: Setup Rust dependencies
+        uses: actions-rs/cargo@v1
         with:
           command: check
 
-      - name: Setup build enc
-        run: |
-          python -m pip install --upgrade wheel setuptools setuptools-rust
-          
-      - name: Install cibuildwheel
-        run: python -m pip install cibuildwheel==2.0.0a2
+      - name: Cache Rust files
+        uses: actions/cache@v2
+        with:
+          path: |
+            ~/.cargo/bin/
+            ~/.cargo/registry/index/
+            ~/.cargo/registry/cache/
+            ~/.cargo/git/db/
+            target/
+          key: ${{ matrix.os }}-${{ matrix.bitness }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+          restore-keys: |
+            ${{ matrix.os }}-${{ matrix.bitness }}-cargo-
+
+      - name: Setup Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: "3.8"
+
+      - name: Cache Python files
+        uses: actions/cache@v2
+        with:
+          path: ${{ matrix.pip-path }}
+          key: ${{ matrix.os }}-${{ matrix.bitness }}-pip-${{ matrix.python }}-${{ matrix.platform_id }}-${{ matrix.manylinux_image }}-${{ hashFiles('**/Cargo.lock') }}
+          restore-keys: |
+            ${{ matrix.os }}-${{ matrix.bitness }}-pip-${{ matrix.python }}-${{ matrix.platform_id }}-${{ matrix.manylinux_image }}-
 
-      - name: Build wheels
-        run: python -m cibuildwheel --output-dir wheelhouse
-        # to supply options, put them in 'env', like:
-        # env:
-        #   CIBW_SOME_OPTION: value
+      - name: Build Python wheels
+        uses: pypa/cibuildwheel@v1.12.0
+        with:
+          package-dir: nlpo3-python
+          output-dir: wheelhouse
+        env:
+          CIBW_BUILD_VERBOSITY: 1
+          # See build selector name at:
+          # https://cibuildwheel.readthedocs.io/en/stable/options/#build-skip
+          CIBW_BUILD: ${{ matrix.python }}-${{ matrix.platform_id }}
+          CIBW_ENVIRONMENT_LINUX: PATH="$HOME/.cargo/bin:$PATH"
+          CIBW_ENVIRONMENT_MACOS: MACOSX_DEPLOYMENT_TARGET=10.13
+                                  PATH="$HOME/.cargo/bin:$PATH"
+                                  CC=/usr/bin/clang
+                                  CXX=/usr/bin/clang++
+          CIBW_MANYLINUX_I686_IMAGE: ${{ matrix.manylinux_image }}
+          CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux_image }}
+          CIBW_MANYLINUX_PYPY_I686_IMAGE: ${{ matrix.manylinux_image }}
+          CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux_image }}
+          CIBW_BEFORE_BUILD_LINUX: >
+            curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain=stable --profile=minimal -y
+          CIBW_BEFORE_BUILD_WINDOWS: 'rustup default stable'
 
-      - uses: actions/upload-artifact@v2
+      - name: Store artifacts
+        uses: actions/upload-artifact@v2
         with:
           path: ./wheelhouse/*.whl
+
+  build_sdist:
+    name: Build Python source distribution
+    runs-on: ubuntu-latest
+    needs: check_build_trigger
+    if: needs.check_build_trigger.outputs.build
+
+    steps:
+      - name: Checkout source code
+        uses: actions/checkout@v2
+
+      - name: Setup Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: "3.8"
+
+      - name: Build source distribution
+        run: |
+          cd nlpo3-python
+          bash ../build_tools/github/build_source.sh
+
+      - name: Store artifacts
+        uses: actions/upload-artifact@v2
+        with:
+          path: nlpo3-python/dist/*.tar.gz
+
+  publish_pypi:
+    name: Publish Python package to PyPI
+    runs-on: ubuntu-latest
+    needs: [build_wheels, build_sdist]
+    # upload to PyPI on every tag starting with 'v'
+    if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v')
+    # alternatively, to publish when a GitHub Release is created, use the following rule:
+    # if: github.event_name == 'release' && github.event.action == 'published'
+
+    steps:
+      - name: Retrieve artifacts
+        uses: actions/download-artifact@v2
+        with:
+          name: artifact
+          path: dist
+
+      - name: Publish package
+        uses: pypa/gh-action-pypi-publish@release/v1
+        with:
+          user: __token__
+          password: ${{ secrets.PYPI_API_TOKEN }}
diff --git a/Cargo.toml b/Cargo.toml
index d48d0ab..e23f406 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,33 +1,33 @@
 [package]
-name = "pythainlp-rust-modules"
-version = "1.1.0"
+name = "nlpo3"
+version = "1.1.1"
 edition = "2018"
-include = ["pyproject.toml"]
+license = "Apache-2.0"
 authors = ["Thanathip Suntorntip Gorlph"]
+description = "Thai natural language processing library, with Python and Node bindings"
+keywords = ["thai", "tokenizer", "nlp", "word-segmentation"]
+categories = ["text-processing"]
+homepage = "https://github.com/PyThaiNLP/oxidized-thainlp/"
+documentation = "https://github.com/PyThaiNLP/oxidized-thainlp/"
+repository = "https://github.com/PyThaiNLP/oxidized-thainlp/"
+readme = "README.md"
+exclude = [".gitignore", ".github/*", "build_tools/*", "tests/*"]
 
 [lib]
-name = "oxidized_thainlp_main_lib"
-path = "src/lib.rs"# "cdylib" is necessary to produce a shared library for Python to import from.
-#
+path = "src/lib.rs"
+# "cdylib" is necessary to produce a shared library for Python to import from.
 # Downstream Rust code (including code in `bin/`, `examples/`, and `tests/`) will not be able
-# to `use string_sum;` unless the "rlib" or "lib" crate type is also included, e.g.:
-# crate-type = ["cdylib", "rlib"]
+# to `use string_sum;` unless the "rlib" or "lib" crate type is also included.
 crate-type = ["cdylib", "rlib"]
 
-[workspace]
-
-members = ["nodejs_binding","python_binding"]
-
-
-
 [dependencies]
-regex = "1.4.6"
-rayon = "1.5"
+ahash = "0.7.2"
 binary-heap-plus = "0.4.1"
-lazy_static = "1.4.0"
 bytecount = "0.6.2"
+lazy_static = "1.4.0"
+rayon = "1.5"
+regex = "1.4.6"
 smol_str = "0.1.17"
-ahash = "0.7.2"
 
 
 [[test]]
diff --git a/README.md b/README.md
index 96eb441..e3d8828 100644
--- a/README.md
+++ b/README.md
@@ -1,71 +1,34 @@
-# oxidized-thainlp
+# nlpO3 (formerly oxidized-thainlp)
 
-Thai Natural Language Processing in Rust, with Python-binding.
+Thai Natural Language Processing library in Rust,
+with Python and Node bindings.
 
 ## Features
 
-- newmm dictionary-based word tokenization, at ultra fast speed
-- support custom dictionary
+- Word tokenizer
+  - maximal-matching dictionary-based tokenization
+  - 2x faster than similar pure Python implementation (PyThaiNLP's newmm)
+  - support custom dictionary
 
 ## Usage
 
-Install:
-```bash
-pip install pythainlp-rust-modules
-```
-
-Use in Python:
-```python
-from oxidized_thainlp import load_dict, segment
-
-load_dict("path/to/dict.file", "dict_name")
-segment("สวัสดีครับ", "dict_name")
-```
-
-Just that!
-
-## Build It Yourself
-
-### Build requirements
-
-- [Rust 2018 Edition](https://www.rust-lang.org/tools/install)
-- Python 3.6 or newer
-- Python Development Headers
-  - Ubuntu: `sudo apt-get install python3-dev`
-  - macOS: No action needed
-- [PyO3](https://github.com/PyO3/pyo3) - already included in Cargo.toml
-- [Maturin](https://github.com/PyO3/maturin) 
-- -  For Windows, use "pip install --pre -U maturin" as of 2021-07-10. See (this Maturin github issue for details)[https://github.com/PyO3/maturin/issues/579] 
+### Bindings
+- [Node.js](nlpo3-nodejs/README.md)
+- [Python](nlpo3-python/README.md)
 
-### Build steps
+### As Rust library
+In `Cargo.toml`:
 
-#### Linux
-```bash
-maturin build --release -i python --many-linux off -m python_binding/Cargo.toml
+```toml
+[dependencies]
+# ...
+nlpo3 = "1.1.1"
 ```
-or
-```bash
-maturin build --release -i python -m python_binding/Cargo.toml
-```
-
-#### Windows (PowerShell)
-```shell
-path\\to\\maturin.exe build --release -i python -m python_binding/Cargo.toml
-```
-
-#### macOS
-```zsh
-maturin build --release -i python3 -m python_binding/Cargo.toml
-```
-
-This should generate a wheel file, in `target/wheels/` directory, which can be installed by pip.
-
-Note: Omitting "-i python" will let Maturin build for all Python versions detected.
 
-## Support
+## Issues
 
-Please report issues at https://github.com/PyThaiNLP/oxidized-thainlp
+Please report issues at https://github.com/PyThaiNLP/oxidized-thainlp/issues
 
-# TODO
+## TODO
 
-Add NodeJS binding
\ No newline at end of file
+- API document.
diff --git a/build_tools/github/build_source.sh b/build_tools/github/build_source.sh
new file mode 100644
index 0000000..bf12861
--- /dev/null
+++ b/build_tools/github/build_source.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+set -e
+set -x
+
+cd ../../
+
+python -m venv build_env
+source build_env/bin/activate
+
+python -m pip install --upgrade pip
+python -m pip install --upgrade setuptools-rust twine
+
+cd oxidized-thainlp/nlpo3-python
+python setup.py sdist
+
+twine check dist/*.tar.gz
diff --git a/build_tools/github/check_build_trigger.sh b/build_tools/github/check_build_trigger.sh
new file mode 100644
index 0000000..c360d3b
--- /dev/null
+++ b/build_tools/github/check_build_trigger.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+
+set -e
+set -x
+
+COMMIT_MSG=$(git log --no-merges -1 --oneline)
+
+# The commit marker "[cd build]" will trigger the build when required
+if [[ "$COMMIT_MSG" =~ "[cd build]" ]]; then
+    echo "::set-output name=build::true"
+fi
diff --git a/nlpo3-nodejs/.gitignore b/nlpo3-nodejs/.gitignore
new file mode 100644
index 0000000..a0dc66e
--- /dev/null
+++ b/nlpo3-nodejs/.gitignore
@@ -0,0 +1,3 @@
+node_modules/
+nlpo3/*.node
+nlpo3/index.js
diff --git a/nodejs_binding/Cargo.toml b/nlpo3-nodejs/Cargo.toml
similarity index 64%
rename from nodejs_binding/Cargo.toml
rename to nlpo3-nodejs/Cargo.toml
index 93562ff..59f6e49 100644
--- a/nodejs_binding/Cargo.toml
+++ b/nlpo3-nodejs/Cargo.toml
@@ -1,20 +1,20 @@
 [package]
-name = "oxidized-nodejs-thainlp"
-version = "0.1.0"
-license = "Apache"
+name = "nlpo3-nodejs"
+version = "0.1.1"
 edition = "2018"
+license = "Apache-2.0"
+authors = ["Thanathip Suntorntip Gorlph"]
 exclude = ["index.node"]
 
 [lib]
 crate-type = ["cdylib"]
 
+[dependencies]
+ahash = "0.7.2"
+lazy_static = "1.4.0"
+nlpo3 = "1.1.1"
+
 [dependencies.neon]
 version = "0.8"
 default-features = false
 features = ["napi-6"]
-
-
-[dependencies]
-lazy_static = "1.4.0"
-ahash = "0.7.2"
-pythainlp-rust-modules = { path = ".." }
\ No newline at end of file
diff --git a/nlpo3-nodejs/README.md b/nlpo3-nodejs/README.md
new file mode 100644
index 0000000..3f6fa94
--- /dev/null
+++ b/nlpo3-nodejs/README.md
@@ -0,0 +1,78 @@
+# nlpO3 Node.js binding
+
+Node.js binding for nlpO3, a Thai natural language processing library in Rust.
+
+## Features
+
+- Word tokenizer
+  - maximal-matching dictionary-based tokenization
+  - 2x faster than similar pure Python implementation (PyThaiNLP's newmm)
+  - support custom dictionary
+
+## Build
+
+### Requirements
+
+- [Rust 2018 Edition](https://www.rust-lang.org/tools/install)
+- Node.js v12 or newer
+
+### Steps
+
+```bash
+# In this directory
+npm run release
+```
+
+Before build, your `nlpo3` directory should look like this
+```
+- nlpo3
+    - index.ts
+    - rust_mod.d.ts
+```
+
+After
+```
+- nlpo3
+    - index.js
+    - index.ts
+    - rust_mod.d.ts
+    - rust_mode.node
+```
+
+## Install
+
+For now, copy the whole `nlpo3` directory after build to your project.
+
+## Usage
+
+In JavaScript:
+```javascript
+const nlpO3 = require(`${path_to_nlpo3}`)
+
+// tokenize a text with default dictionary
+nloO3.segment("สวัสดีครับ")
+
+// load custom dictionary and tokenize with it
+nlpO3.loadDict("path/to/dict.file", "dict_name")
+nloO3.segment("สวัสดีครับ", "dict_name")
+```
+
+In TypeScript:
+```typescript
+import {segment, loadDict} from `${path_to_nlpo3}/index`
+
+// tokenize a text with default dictionary
+segment("สวัสดีครับ")
+
+// load custom dictionary and tokenize with it
+loadDict("path/to/dict.file", "dict_name")
+segment("สวัสดีครับ", "dict_name")
+```
+
+## Issues
+
+Please report issues at https://github.com/PyThaiNLP/oxidized-thainlp/issues
+
+# TODO
+
+- Find a way to build and publish on npm.
diff --git a/nodejs_binding/oxidized_nodejs_thainlp/index.ts b/nlpo3-nodejs/nlpo3/index.ts
similarity index 99%
rename from nodejs_binding/oxidized_nodejs_thainlp/index.ts
rename to nlpo3-nodejs/nlpo3/index.ts
index 7f43854..d9b7d77 100644
--- a/nodejs_binding/oxidized_nodejs_thainlp/index.ts
+++ b/nlpo3-nodejs/nlpo3/index.ts
@@ -4,4 +4,4 @@ export const loadDict = (filePath: string, dictName: string): string => {
 }
 export const segment = (text: string, dictName = 'default', safe = false, parallel = false): string[] => {
     return nativeModule.segment(text, dictName, safe, parallel)
-}
\ No newline at end of file
+}
diff --git a/nodejs_binding/oxidized_nodejs_thainlp/rust_mod.d.ts b/nlpo3-nodejs/nlpo3/rust_mod.d.ts
similarity index 99%
rename from nodejs_binding/oxidized_nodejs_thainlp/rust_mod.d.ts
rename to nlpo3-nodejs/nlpo3/rust_mod.d.ts
index 36b74f4..a74d24d 100644
--- a/nodejs_binding/oxidized_nodejs_thainlp/rust_mod.d.ts
+++ b/nlpo3-nodejs/nlpo3/rust_mod.d.ts
@@ -1,3 +1,2 @@
-
 export function segment(text: string, dict_name: string, safe: boolean, parallel: boolean): string[];
 export function loadDict(file_path: string, dict_name: string): string;
diff --git a/nlpo3-nodejs/package-lock.json b/nlpo3-nodejs/package-lock.json
new file mode 100644
index 0000000..5ea7afe
--- /dev/null
+++ b/nlpo3-nodejs/package-lock.json
@@ -0,0 +1,53 @@
+{
+  "name": "nlpo3",
+  "version": "0.1.0",
+  "lockfileVersion": 2,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "nlpo3",
+      "version": "0.1.0",
+      "hasInstallScript": true,
+      "devDependencies": {
+        "cargo-cp-artifact": "^0.1",
+        "typescript": "^4.3.5"
+      }
+    },
+    "node_modules/cargo-cp-artifact": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/cargo-cp-artifact/-/cargo-cp-artifact-0.1.4.tgz",
+      "integrity": "sha512-34yUas8aUENHGdk6JhLkV4ol0GLtP78YgqpsRDmmnpADy9JoTg/DgKM3CRHAeozTRNhKoPaRFhV+BxoqkmoKUA==",
+      "dev": true,
+      "bin": {
+        "cargo-cp-artifact": "bin/cargo-cp-artifact.js"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "4.3.5",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
+      "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==",
+      "dev": true,
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=4.2.0"
+      }
+    }
+  },
+  "dependencies": {
+    "cargo-cp-artifact": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/cargo-cp-artifact/-/cargo-cp-artifact-0.1.4.tgz",
+      "integrity": "sha512-34yUas8aUENHGdk6JhLkV4ol0GLtP78YgqpsRDmmnpADy9JoTg/DgKM3CRHAeozTRNhKoPaRFhV+BxoqkmoKUA==",
+      "dev": true
+    },
+    "typescript": {
+      "version": "4.3.5",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz",
+      "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==",
+      "dev": true
+    }
+  }
+}
diff --git a/nlpo3-nodejs/package.json b/nlpo3-nodejs/package.json
new file mode 100644
index 0000000..f5b3ce3
--- /dev/null
+++ b/nlpo3-nodejs/package.json
@@ -0,0 +1,33 @@
+{
+  "name": "nlpo3",
+  "version": "0.1.1",
+  "description": "Node.js binding for nlpO3 Thai language processing library",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/PyThaiNLP/oxidized-thainlp.git"
+  },
+  "keywords": [
+    "thai",
+    "tokenizer",
+    "nlp",
+    "rust",
+    "word-segmentation"
+  ],
+  "author": "Thanathip Suntorntip Gorlph",
+  "license": "Apache-2.0",
+  "bugs": {
+    "url": "https://github.com/PyThaiNLP/oxidized-thainlp/issues"
+  },
+  "homepage": "https://github.com/PyThaiNLP/oxidized-thainlp",
+  "main": "./nlpo3/rust_mod.node",
+  "scripts": {
+    "build": "cargo-cp-artifact -nc ./nlpo3/rust_mod.node -- cargo build --message-format=json-render-diagnostics",
+    "release": "cargo-cp-artifact -nc ./nlpo3/rust_mod.node -- cargo build  --release --message-format=json-render-diagnostics && tsc",
+    "install": "npm run build",
+    "test": "cargo test"
+  },
+  "devDependencies": {
+    "cargo-cp-artifact": "^0.1",
+    "typescript": "^4.3.5"
+  }
+}
\ No newline at end of file
diff --git a/nodejs_binding/src/lib.rs b/nlpo3-nodejs/src/lib.rs
similarity index 78%
rename from nodejs_binding/src/lib.rs
rename to nlpo3-nodejs/src/lib.rs
index 46d6a79..3e2f34b 100644
--- a/nodejs_binding/src/lib.rs
+++ b/nlpo3-nodejs/src/lib.rs
@@ -1,23 +1,18 @@
-use neon::prelude::*;
-use oxidized_thainlp_main_lib::tokenizer::{newmm_custom::Newmm, tokenizer_trait::Tokenizer};
-// use oxidized_newmm_tokenizer::tokenizer::newmm_custom::Newmm;
 use ahash::AHashMap as HashMap;
 use lazy_static::lazy_static;
-use oxidized_thainlp_main_lib::tokenizer;
-use std::{error::Error, sync::Mutex};
+use neon::prelude::*;
+use nlpo3::tokenizer::{newmm_custom::Newmm, tokenizer_trait::Tokenizer};
+use std::sync::Mutex;
 lazy_static! {
     static ref  DICT_COLLECTION:Mutex<HashMap<String,Box<Newmm>>> = Mutex::new(HashMap::new());
     // static ref DEFAULT_DICT:Newmm = Newmm::new(None);
 }
 
-
-
 fn load_dict(mut cx: FunctionContext) -> JsResult<JsString> {
-
     let mut dict_col_lock = DICT_COLLECTION.lock().unwrap();
     let file_path = cx.argument::<JsString>(0)?.value(&mut cx);
     let dict_name = cx.argument::<JsString>(1)?.value(&mut cx);
-    if dict_name  == "default" {
+    if dict_name == "default" {
         Ok(cx.string(format!("Failed: 'default' dictionary name is reserved")))
     } else if let Some(_) = dict_col_lock.get(&dict_name) {
         Ok(cx.string(format!(
@@ -34,13 +29,11 @@ fn load_dict(mut cx: FunctionContext) -> JsResult<JsString> {
         )))
     }
 }
-fn segment(
-    mut cx: FunctionContext,
-) -> JsResult<JsArray> {
+fn segment(mut cx: FunctionContext) -> JsResult<JsArray> {
     let text = cx.argument::<JsString>(0)?.value(&mut cx);
     let dict_name = cx.argument::<JsString>(1)?.value(&mut cx);
-    let safe =  cx.argument::<JsBoolean>(2)?.value(&mut cx);
-    let parallel =  cx.argument::<JsBoolean>(3)?.value(&mut cx);
+    let safe = cx.argument::<JsBoolean>(2)?.value(&mut cx);
+    let parallel = cx.argument::<JsBoolean>(3)?.value(&mut cx);
     if let Some(loaded_dict) = DICT_COLLECTION.lock().unwrap().get(&dict_name) {
         let result = loaded_dict.segment_to_string(&text, Some(safe), Some(parallel));
         let js_result_array = JsArray::new(&mut cx, result.len() as u32);
@@ -50,10 +43,7 @@ fn segment(
         }
         Ok(js_result_array)
     } else {
-        panic!(
-            "Dictinary {} does not exist.",
-            dict_name)
-       
+        panic!("Dictinary {} does not exist.", dict_name)
     }
 }
 
@@ -67,4 +57,3 @@ fn main(mut cx: ModuleContext) -> NeonResult<()> {
     cx.export_function("segment", segment)?;
     Ok(())
 }
-
diff --git a/nodejs_binding/tsconfig.json b/nlpo3-nodejs/tsconfig.json
similarity index 97%
rename from nodejs_binding/tsconfig.json
rename to nlpo3-nodejs/tsconfig.json
index e2014f0..27952a5 100644
--- a/nodejs_binding/tsconfig.json
+++ b/nlpo3-nodejs/tsconfig.json
@@ -40,7 +40,7 @@
 
     /* Module Resolution Options */
     // "moduleResolution": "node",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
-    "baseUrl": "./oxidized_nodejs_thainlp",                       /* Base directory to resolve non-absolute module names. */
+    "baseUrl": "nlpo3",                       /* Base directory to resolve non-absolute module names. */
     // "paths": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
     // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
     // "typeRoots": [],                       /* List of folders to include type definitions from. */
diff --git a/nlpo3-python/.cargo/config b/nlpo3-python/.cargo/config
new file mode 100644
index 0000000..d47f983
--- /dev/null
+++ b/nlpo3-python/.cargo/config
@@ -0,0 +1,11 @@
+[target.x86_64-apple-darwin]
+rustflags = [
+  "-C", "link-arg=-undefined",
+  "-C", "link-arg=dynamic_lookup",
+]
+
+[target.aarch64-apple-darwin]
+rustflags = [
+  "-C", "link-arg=-undefined",
+  "-C", "link-arg=dynamic_lookup",
+]
diff --git a/nlpo3-python/Cargo.toml b/nlpo3-python/Cargo.toml
new file mode 100644
index 0000000..d7265b6
--- /dev/null
+++ b/nlpo3-python/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "nlpo3-python"
+version = "1.1.0"
+edition = "2018"
+license = "Apache-2.0"
+authors = ["Thanathip Suntorntip Gorlph"]
+description = "Python binding for nlpO3 Thai language processing library"
+exclude = ["notebooks"]
+
+[lib]
+name = "_nlpo3_python_backend"
+path = "src/lib.rs"
+crate-type = ["cdylib", "rlib"]
+
+[dependencies]
+ahash = "0.7.2"
+lazy_static = "1.4.0"
+nlpo3 = "1.1.1"
+
+[dependencies.pyo3]
+version = "0.13.2"
+features = ["extension-module"]
diff --git a/nlpo3-python/LICENSE b/nlpo3-python/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/nlpo3-python/LICENSE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/nlpo3-python/MANIFEST.in b/nlpo3-python/MANIFEST.in
new file mode 100644
index 0000000..e58d724
--- /dev/null
+++ b/nlpo3-python/MANIFEST.in
@@ -0,0 +1,2 @@
+include LICENSE README.md setup.py pyproject.toml Cargo.toml
+recursive-include src *
diff --git a/nlpo3-python/README.md b/nlpo3-python/README.md
new file mode 100644
index 0000000..bf1c652
--- /dev/null
+++ b/nlpo3-python/README.md
@@ -0,0 +1,58 @@
+# nlpO3 Python binding
+
+Python binding for nlpO3, a Thai natural language processing library in Rust.
+
+## Features
+
+- Word tokenizer
+  - maximal-matching dictionary-based tokenization
+  - 2x faster than similar pure Python implementation (PyThaiNLP's newmm)
+  - support custom dictionary
+
+## Install
+
+```bash
+pip install nlpo3
+```
+
+## Usage
+
+Tokenization using default dictionary:
+```python
+from nlpo3 import segment
+
+segment("สวัสดีครับ")
+```
+
+Load file `path/to/dict.file` to memory and assigned it with name `dict_name`. Then tokenize a text with `dict_name` dictionary:
+```python
+from nlpo3 import load_dict, segment
+
+load_dict("path/to/dict.file", "dict_name")
+segment("สวัสดีครับ", "dict_name")
+```
+
+## Build
+
+### Requirements
+
+- [Rust 2018 Edition](https://www.rust-lang.org/tools/install)
+- Python 3.6 or newer
+- Python Development Headers
+  - Ubuntu: `sudo apt-get install python3-dev`
+  - macOS: No action needed
+- [PyO3](https://github.com/PyO3/pyo3) - already included in Cargo.toml
+- [setuptools-rust](https://github.com/PyO3/setuptools-rust)
+
+### Steps
+
+```bash
+python -m pip install --upgrade build
+python -m build
+```
+
+This should generate a wheel file, in `dist/` directory, which can be installed by pip.
+
+## Issues
+
+Please report issues at https://github.com/PyThaiNLP/oxidized-thainlp/issues
diff --git a/nlpo3-python/build_wheels_macos_local.sh b/nlpo3-python/build_wheels_macos_local.sh
new file mode 100644
index 0000000..748e81d
--- /dev/null
+++ b/nlpo3-python/build_wheels_macos_local.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+
+# The main build wheel workflow is on GitHub,
+# see .github/wheels.yml at the root of main repo.
+# This script is meant to be run local and use for testing purpose only.
+
+# Use pyenv to build on different Python versions.
+
+# If "error: implicit declaration of function 'sendfile' is invalid in C99"
+# occurs when installing a Python version, see fix at:
+# https://github.com/pyenv/pyenv/issues/1740#issuecomment-738749988
+
+set -ex
+
+# store pyenv Python version before calling the script
+SAVE_PYVER=$(pyenv global)
+
+for PYVER in $(ls ~/.pyenv/versions); do
+    PYVER_MINOR=$(echo "${PYVER}" | sed -nre 's/^(pypy)?(([0-9]+\.)?[0-9]+).*/\1\2/p')
+    echo "Build for Python ${PYVER_MINOR}"
+    pyenv global "${PYVER}"
+    $(pyenv which pip) install -U pip
+    $(pyenv which pip) install -U build setuptools setuptools-rust wheel
+    $(pyenv which python) -m build --wheel
+done
+
+# restore pyenv Python version
+pyenv global ${SAVE_PYVER}
diff --git a/nlpo3-python/build_wheels_manylinux_local.sh b/nlpo3-python/build_wheels_manylinux_local.sh
new file mode 100644
index 0000000..5ee50d8
--- /dev/null
+++ b/nlpo3-python/build_wheels_manylinux_local.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+
+# The main build wheel workflow is on GitHub,
+# see .github/wheels.yml at the root of main repo.
+# This script is meant to be run local and use for testing purpose only.
+
+# This script has to run through manylinux docker image:
+# docker run --rm -v `pwd`:/io quay.io/pypa/manylinux2014_x86_64 bash /io/build_wheels_manylinux_local.sh
+
+set -ex
+
+curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain stable -y
+export PATH="$HOME/.cargo/bin:$PATH"
+
+cd /io
+
+for PYBIN in /opt/python/cp{36,37,38,39}*/bin; do
+    "${PYBIN}/pip" install -U build setuptools setuptools-rust wheel
+    "${PYBIN}/python" -m build --wheel
+done
+
+for whl in dist/*linux*.whl; do
+    auditwheel repair "$whl" -w dist/
+done
diff --git a/python_binding/oxidized_thainlp/__init__.py b/nlpo3-python/nlpo3/__init__.py
similarity index 88%
rename from python_binding/oxidized_thainlp/__init__.py
rename to nlpo3-python/nlpo3/__init__.py
index 372451f..c14114b 100644
--- a/python_binding/oxidized_thainlp/__init__.py
+++ b/nlpo3-python/nlpo3/__init__.py
@@ -1,5 +1,6 @@
-from .oxidized_thainlp import (
-    load_dict as load_dict_to_oxidized,
+# import from .so (Rust)
+from ._nlpo3_python_backend import (
+    load_dict as rust_load_dict,
     segment as rust_segment,
 )
 from typing import List
@@ -14,7 +15,7 @@ def load_dict(file_path: str, dict_name: str):
         Can be any valid utf-8 string, except "default" which is reserved.
     :type dict_name: str
     """
-    load_result = load_dict_to_oxidized(file_path, dict_name)
+    load_result = rust_load_dict(file_path, dict_name)
     print(load_result)
 
 
diff --git a/nlpo3-python/notebooks/nlpo3_segment_benchmarks.ipynb b/nlpo3-python/notebooks/nlpo3_segment_benchmarks.ipynb
new file mode 100644
index 0000000..86b0d01
--- /dev/null
+++ b/nlpo3-python/notebooks/nlpo3_segment_benchmarks.ipynb
@@ -0,0 +1,675 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {
+    "id": "6PCaAU1y66FE"
+   },
+   "source": [
+    "# Benchmark `nlpo3.segment`"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Benchmarks nlpO3 (Rust) `nlpo3.segment` (unsafe) against PyThaiNLP (Python) `newmm.segment` (unsafe) using the same dictionary.\n",
+    "\n",
+    "https://github.com/PyThaiNLP/oxidized-thainlp/"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {
+    "id": "iAlScT9d66FX",
+    "outputId": "fe7a569c-f384-4e95-cbc3-3d412a99fc74"
+   },
+   "outputs": [],
+   "source": [
+    "import time\n",
+    "\n",
+    "import matplotlib.pyplot as plt\n",
+    "import numpy as np\n",
+    "from tqdm.auto import tqdm\n",
+    "\n",
+    "\n",
+    "def time_func(func, arg):\n",
+    "    start_time = time.time()\n",
+    "    func(arg)\n",
+    "    return time.time() - start_time"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {
+    "id": "Ssp84MKA66Fb"
+   },
+   "source": [
+    "## Load Custom Dictionary to the Tokenizers\n",
+    "\n",
+    "Both `o3_newmm()` and `py_newmm()` will use the same word list (`words_th.txt`)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {
+    "id": "XHh2LIdG66Fd",
+    "outputId": "f1f8c12d-fd61-40f2-d31f-98c99cd120ae"
+   },
+   "outputs": [],
+   "source": [
+    "DICT_FILE = \"../../words_th.txt\""
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from pythainlp.tokenize.newmm import segment as py_segment\n",
+    "from pythainlp.util import dict_trie\n",
+    "\n",
+    "trie = dict_trie(dict_source=DICT_FILE)\n",
+    "\n",
+    "def py_newmm(txt, safe_mode=False):\n",
+    "    return py_segment(txt, safe_mode=safe_mode, custom_dict=trie)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "Successful: dictionary name test_dict from file ../../words_th.txt has been successfully loaded\n"
+     ]
+    }
+   ],
+   "source": [
+    "from nlpo3 import load_dict\n",
+    "from nlpo3 import segment as o3_segment\n",
+    "\n",
+    "load_dict(DICT_FILE, \"test_dict\")  # create \"test_dict\" dictionary\n",
+    "\n",
+    "def o3_newmm(txt, safe=False, parallel=False):\n",
+    "    return o3_segment(txt, dict_name=\"test_dict\", safe=safe, parallel=parallel)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Load Test data"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stderr",
+     "output_type": "stream",
+     "text": [
+      "Reusing dataset wisesight_sentiment (/Users/arthit/.cache/huggingface/datasets/wisesight_sentiment/wisesight_sentiment/1.0.0/fc2b1bdfe79571b2e281e4afdb5aac069cf9270bf0f85694239be672a4191969)\n"
+     ]
+    },
+    {
+     "data": {
+      "text/plain": [
+       "DatasetDict({\n",
+       "    train: Dataset({\n",
+       "        features: ['category', 'texts'],\n",
+       "        num_rows: 21628\n",
+       "    })\n",
+       "    validation: Dataset({\n",
+       "        features: ['category', 'texts'],\n",
+       "        num_rows: 2404\n",
+       "    })\n",
+       "    test: Dataset({\n",
+       "        features: ['category', 'texts'],\n",
+       "        num_rows: 2671\n",
+       "    })\n",
+       "})"
+      ]
+     },
+     "execution_count": 5,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "from datasets import load_dataset\n",
+    "\n",
+    "datasets = load_dataset('wisesight_sentiment')\n",
+    "datasets"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "metadata": {
+    "id": "GCtUUACp66Fc",
+    "outputId": "fe1c7236-28b3-4ee4-a0e4-81312e7762fb"
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "'ไปจองมาแล้วนาจา Mitsubishi Attrage ได้หลังสงกรานต์เลย รอขับอยู่นาจา กระทัดรัด เหมาะกับสาวๆขับรถคนเดียวแบบเรา ราคาสบายกระเป๋า ประหยัดน้ำมัน วิ่งไกลแค่ไหนหายห่วงค่ะ'"
+      ]
+     },
+     "execution_count": 6,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "txt = datasets['train']['texts'][0]\n",
+    "txt"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "['ไป', 'จอง', 'มา', 'แล้', 'วนา', 'จา', ' ', 'Mitsubishi', ' ', 'Attrage']"
+      ]
+     },
+     "execution_count": 7,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "py_newmm(txt)[:10]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "['ไป', 'จอง', 'มา', 'แล้', 'วนา', 'จา', ' ', 'Mitsubishi', ' ', 'Attrage']"
+      ]
+     },
+     "execution_count": 8,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "o3_newmm(txt)[:10]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {
+    "id": "Qyrh-uny66Fh"
+   },
+   "source": [
+    "## One Example"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {
+    "id": "9YqrA2Pb66Fj"
+   },
+   "source": [
+    "### Average Run Time for One Example"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "metadata": {
+    "id": "Iz58c6Ff66Fj",
+    "outputId": "0d45dec2-bece-494e-8725-31ffdc97e1de"
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "313 µs ± 7.57 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)\n"
+     ]
+    }
+   ],
+   "source": [
+    "%timeit py_newmm(txt)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 10,
+   "metadata": {
+    "id": "8jvijfPJ66Fl",
+    "outputId": "64a89f5b-468e-4af5-da35-6c938019e021"
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "130 µs ± 5.65 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)\n"
+     ]
+    }
+   ],
+   "source": [
+    "%timeit o3_newmm(txt)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 11,
+   "metadata": {
+    "id": "lAdLgqdu66Fm",
+    "outputId": "42e39b71-9331-4311-a401-61fa68c21fde"
+   },
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "219 µs ± 15.3 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)\n"
+     ]
+    }
+   ],
+   "source": [
+    "%timeit o3_newmm(txt, parallel=True)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 12,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "328 µs ± 10.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)\n"
+     ]
+    }
+   ],
+   "source": [
+    "%timeit py_newmm(txt, safe_mode=True)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 13,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "134 µs ± 2.02 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)\n"
+     ]
+    }
+   ],
+   "source": [
+    "%timeit o3_newmm(txt, safe=True)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {
+    "id": "L0lWERZk66Fm"
+   },
+   "source": [
+    "## All Examples"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {
+    "id": "GXmhzISZ66Fn"
+   },
+   "source": [
+    "### Check If Results Match for All Examples"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 14,
+   "metadata": {
+    "id": "ZY9Mosag66Fn",
+    "outputId": "46a56ce0-fd20-430a-d9b6-9564f4c25141"
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "(0.9999075272794525, 21628)"
+      ]
+     },
+     "execution_count": 14,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "corrects = [o3_newmm(txt) == py_newmm(txt) for txt in datasets['train']['texts']]\n",
+    "np.mean(corrects), len(corrects)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {
+    "id": "Tyc_cHaf66Fo"
+   },
+   "source": [
+    "### Average Run Time Across All Examples"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 15,
+   "metadata": {
+    "id": "uYeUydsQ66Fo",
+    "outputId": "b20ed761-fa2b-42b8-8a6c-ff3a1d2dc6b3"
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "(0.00020515467866150095, 8.277630043135611e-05, 2.4784229011494605)"
+      ]
+     },
+     "execution_count": 15,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "py_newmms = [time_func(py_newmm, txt) for txt in datasets['train']['texts']]\n",
+    "o3_newmms = [time_func(o3_newmm, txt) for txt in datasets['train']['texts']]\n",
+    "\n",
+    "# o3 newmm is over 2x faster than python newmm, on average\n",
+    "np.mean(py_newmms), np.mean(o3_newmms), np.mean(py_newmms) / np.mean(o3_newmms)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 16,
+   "metadata": {
+    "id": "8hRoDxm966Fp",
+    "outputId": "c4e8c0fd-97ca-4e3a-ee63-1281f84bb1d9"
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "(array([1.4746e+04, 3.4960e+03, 1.2480e+03, 8.9000e+02, 4.3000e+02,\n",
+       "        2.3500e+02, 1.4700e+02, 1.0400e+02, 8.0000e+01, 4.7000e+01,\n",
+       "        4.7000e+01, 3.7000e+01, 1.7000e+01, 1.8000e+01, 1.2000e+01,\n",
+       "        1.1000e+01, 7.0000e+00, 8.0000e+00, 9.0000e+00, 5.0000e+00,\n",
+       "        4.0000e+00, 1.2000e+01, 3.0000e+00, 3.0000e+00, 4.0000e+00,\n",
+       "        2.0000e+00, 1.0000e+00, 2.0000e+00, 1.0000e+00, 2.0000e+00]),\n",
+       " array([3.81469727e-06, 6.98169072e-05, 1.35819117e-04, 2.01821327e-04,\n",
+       "        2.67823537e-04, 3.33825747e-04, 3.99827957e-04, 4.65830167e-04,\n",
+       "        5.31832377e-04, 5.97834587e-04, 6.63836797e-04, 7.29839007e-04,\n",
+       "        7.95841217e-04, 8.61843427e-04, 9.27845637e-04, 9.93847847e-04,\n",
+       "        1.05985006e-03, 1.12585227e-03, 1.19185448e-03, 1.25785669e-03,\n",
+       "        1.32385890e-03, 1.38986111e-03, 1.45586332e-03, 1.52186553e-03,\n",
+       "        1.58786774e-03, 1.65386995e-03, 1.71987216e-03, 1.78587437e-03,\n",
+       "        1.85187658e-03, 1.91787879e-03, 1.98388100e-03]),\n",
+       " <BarContainer object of 30 artists>)"
+      ]
+     },
+     "execution_count": 16,
+     "metadata": {},
+     "output_type": "execute_result"
+    },
+    {
+     "data": {
+      "image/png": "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\n",
+      "text/plain": [
+       "<Figure size 432x288 with 1 Axes>"
+      ]
+     },
+     "metadata": {
+      "needs_background": "light"
+     },
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "# look at distribution; o3 newmm also consistently performs better\n",
+    "plt.hist(py_newmms, bins=30, alpha=0.5)\n",
+    "plt.hist(o3_newmms, bins=30, alpha=0.5)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {
+    "id": "EMZZ8SgY66Fp"
+   },
+   "source": [
+    "## Run Time as Sequence Length Grows"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 17,
+   "metadata": {
+    "id": "3P_z59rS66Fp",
+    "outputId": "5951dd75-388c-4f9e-a1df-f0e98f663ec3"
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "('เปิดศักราชใหม่! นายกฯ แถลงข่าวก่อนการแข่งขัน', 10)"
+      ]
+     },
+     "execution_count": 17,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "txt = datasets['train']['texts'][1]\n",
+    "txt2 = ''.join(o3_newmm(txt)[:10])\n",
+    "txt2, len(o3_newmm(txt2))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 18,
+   "metadata": {
+    "colab": {
+     "referenced_widgets": [
+      "0689a5cf946049a0ac98bdf9e1353810",
+      "2b481450056f4c1883c163bf066110a3"
+     ]
+    },
+    "id": "FEmDkPHL66Fq",
+    "outputId": "2b2c96ab-7044-423b-9b40-e06ca186a213"
+   },
+   "outputs": [
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "ed20eb28ecbd4ad8a78b0bf67cdbb840",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "HBox(children=(FloatProgress(value=0.0, max=5.0), HTML(value='')))"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\n"
+     ]
+    },
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "95c4b518040b43118553bef5dad8526e",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "HBox(children=(FloatProgress(value=0.0, max=5.0), HTML(value='')))"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    },
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "\n"
+     ]
+    }
+   ],
+   "source": [
+    "py_newmms = [time_func(py_newmm, txt2*i) for i in tqdm([10**j for j in range(5)])]\n",
+    "o3_newmms = [time_func(o3_newmm, txt2*i) for i in tqdm([10**j for j in range(5)])]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Performance starts really deviate when sequence length > 10^3 tokens and above.\n",
+    "\n",
+    "python newmm is dashed line."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 19,
+   "metadata": {
+    "id": "sT8GL0oX66Fr",
+    "outputId": "6bb4acfd-4721-47cb-d8ff-943c67a4cedf"
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[<matplotlib.lines.Line2D at 0x1317b8f10>]"
+      ]
+     },
+     "execution_count": 19,
+     "metadata": {},
+     "output_type": "execute_result"
+    },
+    {
+     "data": {
+      "image/png": "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\n",
+      "text/plain": [
+       "<Figure size 432x288 with 1 Axes>"
+      ]
+     },
+     "metadata": {
+      "needs_background": "light"
+     },
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "positions = [i for i in range(5)]\n",
+    "labels = [f'10^{i+1}' for i in range(5)]\n",
+    "plt.xticks(positions, labels)\n",
+    "plt.plot(py_newmms, linestyle='dashed')\n",
+    "plt.plot(o3_newmms)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 20,
+   "metadata": {
+    "id": "5YU5aiNs66Fs"
+   },
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "[<matplotlib.lines.Line2D at 0x13152f7c0>]"
+      ]
+     },
+     "execution_count": 20,
+     "metadata": {},
+     "output_type": "execute_result"
+    },
+    {
+     "data": {
+      "image/png": "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\n",
+      "text/plain": [
+       "<Figure size 432x288 with 1 Axes>"
+      ]
+     },
+     "metadata": {
+      "needs_background": "light"
+     },
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "# zooming in on inflexion point\n",
+    "positions = [i for i in range(4)]\n",
+    "labels = [f'10^{i+1}' for i in range(4)]\n",
+    "plt.xticks(positions, labels)\n",
+    "plt.plot(py_newmms[:-1], linestyle='dashed')\n",
+    "plt.plot(o3_newmms[:-1])"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "colab": {
+   "name": "oxidized_segment.ipynb",
+   "provenance": []
+  },
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.9.6"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 1
+}
diff --git a/nlpo3-python/pyproject.toml b/nlpo3-python/pyproject.toml
new file mode 100644
index 0000000..3e8c05f
--- /dev/null
+++ b/nlpo3-python/pyproject.toml
@@ -0,0 +1,54 @@
+[build-system]
+requires = ["setuptools", "setuptools-rust", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "nlpo3"
+version = "1.1.0"
+description = "Python binding for nlpO3 Thai language processing library in Rust"
+readme = "README.md"
+requires-python = ">=3.6"
+license = "Apache-2.0"
+keywords = ["thai", "tokenizer", "nlp", "rust", "pythainlp"]
+authors = [
+  { name = "Thanathip Suntorntip" },
+  { name = "Arthit Suriyawongkul" },
+  { name = "Wannaphong Phatthiyaphaibun" },
+]
+classifiers = [
+  "Development Status :: 5 - Production/Stable",
+  "Programming Language :: Python :: 3",
+  "Intended Audience :: Developers",
+  "License :: OSI Approved :: Apache Software License",
+  "Natural Language :: Thai",
+  "Topic :: Scientific/Engineering :: Artificial Intelligence",
+  "Topic :: Text Processing :: Linguistic",
+  "Topic :: Software Development :: Libraries :: Python Modules",
+]
+
+[project.urls]
+homepage = "https://github.com/PyThaiNLP/oxidized-thainlp"
+repository = "https://github.com/PyThaiNLP/oxidized-thainlp"
+
+[tool.poetry.dependencies]
+python = "^3.6"
+
+[tool.poetry.dev-dependencies]
+pytest = "*"
+pytest-runner = "*"
+wheel = "*"
+
+[tool.black]
+line-length = 79
+target_version = ['py36', 'py37', 'py38', 'py39']
+experimental_string_processing = true
+exclude = '''
+/(
+    \.eggs
+  | \.git
+  | \.mypy_cache
+  | \.vscode
+  | build
+  | dist
+)/
+'''
diff --git a/nlpo3-python/setup.py b/nlpo3-python/setup.py
new file mode 100644
index 0000000..fb9cec5
--- /dev/null
+++ b/nlpo3-python/setup.py
@@ -0,0 +1,83 @@
+from setuptools import find_packages, setup
+from setuptools_rust import Binding, RustExtension, Strip
+
+long_description = """
+Python binding for nlpO3, a Thai natural language processing library in Rust.
+
+## Features
+
+- Word tokenizer
+  - maximal-matching dictionary-based tokenization
+  - 2x faster than similar pure Python implementation (PyThaiNLP's newmm)
+  - support custom dictionary
+
+## Install
+
+```bash
+pip install nlpo3
+```
+
+## Usage
+
+Tokenization using default dictionary:
+```python
+from nlpo3 import segment
+
+segment("สวัสดีครับ")
+```
+
+Load file `path/to/dict.file` to memory and assigned it with name `dict_name`.
+Then tokenize a text with `dict_name` dictionary:
+```python
+from nlpo3 import load_dict, segment
+
+load_dict("path/to/dict.file", "dict_name")
+segment("สวัสดีครับ", "dict_name")
+"""
+
+setup(
+    name="nlpo3",
+    version="1.1.0",
+    description=(
+        "Python binding for nlpO3 Thai language processing library"
+    ),
+    long_description=long_description,
+    long_description_content_type="text/markdown",
+    python_requires=">=3.6",
+    license="Apache-2.0",
+    keywords=[
+        "thai",
+        "tokenizer",
+        "nlp",
+        "rust",
+        "pythainlp",
+    ],
+    author=(
+        "Thanathip Suntorntip, "
+        "Arthit Suriyawongkul, "
+        "Wannaphong Phatthiyaphaibun"
+    ),
+    author_email="wannaphong@yahoo.com",
+    url="https://github.com/PyThaiNLP/oxidized-thainlp/",
+    classifiers=[
+        "Development Status :: 5 - Production/Stable",
+        "Programming Language :: Python :: 3",
+        "Intended Audience :: Developers",
+        "License :: OSI Approved :: Apache Software License",
+        "Natural Language :: Thai",
+        "Topic :: Scientific/Engineering :: Artificial Intelligence",
+        "Topic :: Text Processing :: Linguistic",
+        "Topic :: Software Development :: Libraries :: Python Modules",
+    ],
+    include_package_data=True,
+    packages=find_packages(),
+    zip_safe=False,
+    rust_extensions=[
+        RustExtension(
+            "nlpo3._nlpo3_python_backend",
+            path="Cargo.toml",
+            binding=Binding.PyO3,
+            strip=Strip.No,
+        )
+    ],
+)
diff --git a/python_binding/src/lib.rs b/nlpo3-python/src/lib.rs
similarity index 79%
rename from python_binding/src/lib.rs
rename to nlpo3-python/src/lib.rs
index 38e4904..de5c7d0 100644
--- a/python_binding/src/lib.rs
+++ b/nlpo3-python/src/lib.rs
@@ -1,20 +1,18 @@
-use oxidized_thainlp_main_lib::tokenizer::newmm_custom::Newmm;
-// use oxidized_newmm_tokenizer::tokenizer::newmm_custom::Newmm;
-use oxidized_thainlp_main_lib::tokenizer;
-// ีuse oxidized_newmm_tokenizer::tokenizer::newmm_custom::Newmm;
 use ahash::AHashMap as HashMap;
 use lazy_static::lazy_static;
+use nlpo3::tokenizer;
+use nlpo3::tokenizer::newmm_custom::Newmm;
 use pyo3::prelude::*;
-use pyo3::types::{PyString};
+use pyo3::types::PyString;
 use pyo3::{exceptions, wrap_pyfunction};
 use std::sync::Mutex;
 use tokenizer::tokenizer_trait::Tokenizer;
 lazy_static! {
-    static ref  DICT_COLLECTION:Mutex<HashMap<String,Box<Newmm>>> = Mutex::new(HashMap::new());
+    static ref DICT_COLLECTION:Mutex<HashMap<String,Box<Newmm>>> = Mutex::new(HashMap::new());
     // static ref DEFAULT_DICT:Newmm = Newmm::new(None);
 }
 
-// / segment(text,dict_name, safe, parallel, /)
+// / segment(text, dict_name, safe, parallel, /)
 // / --
 // /
 // / This function is newmm algorithhm.
@@ -30,7 +28,6 @@ fn segment(
     safe: Option<bool>,
     parallel: Option<bool>,
 ) -> PyResult<Vec<String>> {
-    // let utf8_text = std::str::from_utf8(text.as_bytes())?; 
     if let Some(loaded_dict) = DICT_COLLECTION.lock().unwrap().get(dict_name) {
         let result = loaded_dict.segment_to_string(text.to_str()?, safe, parallel);
         Ok(result)
@@ -42,7 +39,7 @@ fn segment(
     }
 }
 
-/// load_dict(file_path,dict_name /)
+/// load_dict(file_path, dict_name /)
 /// --
 ///
 /// This function loads a dictionary file and add it to dict collection
@@ -69,8 +66,9 @@ fn load_dict(file_path: &str, dict_name: &str) -> PyResult<String> {
         ))
     }
 }
+
 #[pymodule]
-fn oxidized_thainlp(_py: Python, m: &PyModule) -> PyResult<()> {
+fn _nlpo3_python_backend(_py: Python, m: &PyModule) -> PyResult<()> {
     {
         let mut dict_collect = DICT_COLLECTION.lock().unwrap();
         dict_collect.insert("default".to_string(), Box::from(Newmm::new(None)));
diff --git a/nodejs_binding/.gitignore b/nodejs_binding/.gitignore
deleted file mode 100644
index 1145a43..0000000
--- a/nodejs_binding/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-node_modules/
-oxidized_nodejs_thainlp/*.node
\ No newline at end of file
diff --git a/nodejs_binding/README.md b/nodejs_binding/README.md
deleted file mode 100644
index 783f8b1..0000000
--- a/nodejs_binding/README.md
+++ /dev/null
@@ -1,75 +0,0 @@
-# oxidized-nodejs-thainlp
-
-Like the root directory, but with nodejs binding.
-
-## Features
-
-- newmm dictionary-based word tokenization, at ultra fast speed
-- support custom dictionary
-
-## Build It Yourself
-
-### Build requirements
-
-- [Rust 2018 Edition](https://www.rust-lang.org/tools/install)
-- NodeJS V12 ++
-
-### Build steps
-1.
-```bash
-# In this directory
-npm run release
-
-```
-Before build, your oxidized_nodejs_thainlp should look like this
-```
-- oxidized_nodejs_thainlp
-    - rust_mod.d.ts
-    - index.ts
-
-```
-After
-```
-- oxidized_nodejs_thainlp
-    - rust_mod.d.ts
-    - rust_mode.node
-    - index.ts
-    - index.js
-
-```
-## Usage
-
-For now, copy the whole oxidized_nodejs_thainlp directory after build to your project 
-
-In NodeJS:
-```javascript
-const  oxidizedThaiNLP = require(`${path_to_oxidized_nodejs_thainlp_directory}`)
-
-oxidizedThaiNLP.loadDict("path/to/dict.file", "dict_name")
-oxidizedThaiNLP.segment("สวัสดีครับ", "dict_name")
-```
-
-In TypeScript (run on node)
-
-```typescript
-
-import {segment,loadDict} from `${path_to_oxidized_nodejs_thainlp_directory}/index`
-
-loadDict("path/to/dict.file", "dict_name")
-segment("สวัสดีครับ", "dict_name")
-```
-
-
-Just that!
-
-
-## Support
-
-Please report issues at https://github.com/PyThaiNLP/oxidized-thainlp
-
-# TODO
-
-Find a way to build and publish on npm.
-
-
-
diff --git a/nodejs_binding/package.json b/nodejs_binding/package.json
deleted file mode 100644
index f7252ca..0000000
--- a/nodejs_binding/package.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
-  "name": "oxidized-nodejs-thainlp",
-  "version": "0.1.0",
-  "description": "",
-  "main": "./oxidized_nodejs_thainlp/rust_mod.node",
-  "scripts": {
-    "build": "cargo-cp-artifact -nc ./oxidized_nodejs_thainlp/rust_mod.node -- cargo build --message-format=json-render-diagnostics",
-    "release": "cargo-cp-artifact -nc ./oxidized_nodejs_thainlp/rust_mod.node -- cargo build  --release --message-format=json-render-diagnostics && tsc",
-    "install": "npm run build",
-    "test": "cargo test"
-  },
-  "author": "",
-
-  "devDependencies": {
-    "cargo-cp-artifact": "^0.1",
-    "typescript": "^4.3.5"
-  }
-}
diff --git a/notebooks/oxidized_segment_benchmarks.ipynb b/notebooks/oxidized_segment_benchmarks.ipynb
deleted file mode 100644
index 9c07619..0000000
--- a/notebooks/oxidized_segment_benchmarks.ipynb
+++ /dev/null
@@ -1,675 +0,0 @@
-{
- "cells": [
-  {
-   "cell_type": "markdown",
-   "metadata": {
-    "id": "6PCaAU1y66FE"
-   },
-   "source": [
-    "# Benchmark `oxidized_thainlp.segment`"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "Benchmarks Rust `oxidized_thainlp.segment` (unsafe) against Python `newmm.segment` (unsafe) using the same dictionary.\n",
-    "\n",
-    "https://github.com/PyThaiNLP/oxidized-thainlp/"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 1,
-   "metadata": {
-    "id": "iAlScT9d66FX",
-    "outputId": "fe7a569c-f384-4e95-cbc3-3d412a99fc74"
-   },
-   "outputs": [],
-   "source": [
-    "import time\n",
-    "\n",
-    "import matplotlib.pyplot as plt\n",
-    "import numpy as np\n",
-    "from tqdm.auto import tqdm\n",
-    "\n",
-    "\n",
-    "def time_func(func, arg):\n",
-    "    start_time = time.time()\n",
-    "    func(arg)\n",
-    "    return time.time() - start_time"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {
-    "id": "Ssp84MKA66Fb"
-   },
-   "source": [
-    "## Load Custom Dictionary to the Tokenizers\n",
-    "\n",
-    "Both `oxidized_newmm()` and `py_newmm()` will use the same word list (`words_th.txt`)"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 2,
-   "metadata": {
-    "id": "XHh2LIdG66Fd",
-    "outputId": "f1f8c12d-fd61-40f2-d31f-98c99cd120ae"
-   },
-   "outputs": [],
-   "source": [
-    "DICT_FILE = \"../words_th.txt\""
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 3,
-   "metadata": {},
-   "outputs": [],
-   "source": [
-    "from pythainlp.tokenize.newmm import segment as py_segment\n",
-    "from pythainlp.util import dict_trie\n",
-    "\n",
-    "trie = dict_trie(dict_source=DICT_FILE)\n",
-    "\n",
-    "def py_newmm(txt, safe_mode=False):\n",
-    "    return py_segment(txt, safe_mode=safe_mode, custom_dict=trie)"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 4,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "Successful: dictionary name test_dict from file ../words_th.txt has been successfully loaded\n"
-     ]
-    }
-   ],
-   "source": [
-    "from oxidized_thainlp import load_dict\n",
-    "from oxidized_thainlp import segment as oxidized_segment\n",
-    "\n",
-    "load_dict(DICT_FILE, \"test_dict\")  # create \"test_dict\" dictionary\n",
-    "\n",
-    "def oxidized_newmm(txt, safe=False, parallel=False):\n",
-    "    return oxidized_segment(txt, dict_name=\"test_dict\", safe=safe, parallel=parallel)"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "## Load Test data"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 5,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stderr",
-     "output_type": "stream",
-     "text": [
-      "Reusing dataset wisesight_sentiment (/Users/test/.cache/huggingface/datasets/wisesight_sentiment/wisesight_sentiment/1.0.0/fc2b1bdfe79571b2e281e4afdb5aac069cf9270bf0f85694239be672a4191969)\n"
-     ]
-    },
-    {
-     "data": {
-      "text/plain": [
-       "DatasetDict({\n",
-       "    train: Dataset({\n",
-       "        features: ['category', 'texts'],\n",
-       "        num_rows: 21628\n",
-       "    })\n",
-       "    validation: Dataset({\n",
-       "        features: ['category', 'texts'],\n",
-       "        num_rows: 2404\n",
-       "    })\n",
-       "    test: Dataset({\n",
-       "        features: ['category', 'texts'],\n",
-       "        num_rows: 2671\n",
-       "    })\n",
-       "})"
-      ]
-     },
-     "execution_count": 5,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "from datasets import load_dataset\n",
-    "\n",
-    "datasets = load_dataset('wisesight_sentiment')\n",
-    "datasets"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 6,
-   "metadata": {
-    "id": "GCtUUACp66Fc",
-    "outputId": "fe1c7236-28b3-4ee4-a0e4-81312e7762fb"
-   },
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "'ไปจองมาแล้วนาจา Mitsubishi Attrage ได้หลังสงกรานต์เลย รอขับอยู่นาจา กระทัดรัด เหมาะกับสาวๆขับรถคนเดียวแบบเรา ราคาสบายกระเป๋า ประหยัดน้ำมัน วิ่งไกลแค่ไหนหายห่วงค่ะ'"
-      ]
-     },
-     "execution_count": 6,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "txt = datasets['train']['texts'][0]\n",
-    "txt"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 7,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "['ไป', 'จอง', 'มา', 'แล้', 'วนา', 'จา', ' ', 'Mitsubishi', ' ', 'Attrage']"
-      ]
-     },
-     "execution_count": 7,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "py_newmm(txt)[:10]"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 8,
-   "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "['ไป', 'จอง', 'มา', 'แล้', 'วนา', 'จา', ' ', 'Mitsubishi', ' ', 'Attrage']"
-      ]
-     },
-     "execution_count": 8,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "oxidized_newmm(txt)[:10]"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {
-    "id": "Qyrh-uny66Fh"
-   },
-   "source": [
-    "## One Example"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {
-    "id": "9YqrA2Pb66Fj"
-   },
-   "source": [
-    "### Average Run Time for One Example"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 9,
-   "metadata": {
-    "id": "Iz58c6Ff66Fj",
-    "outputId": "0d45dec2-bece-494e-8725-31ffdc97e1de"
-   },
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "306 µs ± 7.78 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)\n"
-     ]
-    }
-   ],
-   "source": [
-    "%timeit py_newmm(txt)"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 10,
-   "metadata": {
-    "id": "8jvijfPJ66Fl",
-    "outputId": "64a89f5b-468e-4af5-da35-6c938019e021"
-   },
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "172 µs ± 2.58 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)\n"
-     ]
-    }
-   ],
-   "source": [
-    "%timeit oxidized_newmm(txt)"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 11,
-   "metadata": {
-    "id": "lAdLgqdu66Fm",
-    "outputId": "42e39b71-9331-4311-a401-61fa68c21fde"
-   },
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "246 µs ± 4.55 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)\n"
-     ]
-    }
-   ],
-   "source": [
-    "%timeit oxidized_newmm(txt, parallel=True)"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 12,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "306 µs ± 3.41 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)\n"
-     ]
-    }
-   ],
-   "source": [
-    "%timeit py_newmm(txt, safe_mode=True)"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 13,
-   "metadata": {},
-   "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "178 µs ± 3.65 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)\n"
-     ]
-    }
-   ],
-   "source": [
-    "%timeit oxidized_newmm(txt, safe=True)"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {
-    "id": "L0lWERZk66Fm"
-   },
-   "source": [
-    "## All Examples"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {
-    "id": "GXmhzISZ66Fn"
-   },
-   "source": [
-    "### Check If Results Match for All Examples"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 14,
-   "metadata": {
-    "id": "ZY9Mosag66Fn",
-    "outputId": "46a56ce0-fd20-430a-d9b6-9564f4c25141"
-   },
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "(0.9999075272794525, 21628)"
-      ]
-     },
-     "execution_count": 14,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "corrects = [oxidized_newmm(txt) == py_newmm(txt) for txt in datasets['train']['texts']]\n",
-    "np.mean(corrects), len(corrects)"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {
-    "id": "Tyc_cHaf66Fo"
-   },
-   "source": [
-    "### Average Run Time Across All Examples"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 15,
-   "metadata": {
-    "id": "uYeUydsQ66Fo",
-    "outputId": "b20ed761-fa2b-42b8-8a6c-ff3a1d2dc6b3"
-   },
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "(0.00019629587632396733, 0.00010808309654177988, 1.8161570366193989)"
-      ]
-     },
-     "execution_count": 15,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "py_newmms = [time_func(py_newmm, txt) for txt in datasets['train']['texts']]\n",
-    "oxidized_newmms = [time_func(oxidized_newmm, txt) for txt in datasets['train']['texts']]\n",
-    "\n",
-    "# oxidized newmm is nearly 2 times faster than python newmm, on average\n",
-    "np.mean(py_newmms), np.mean(oxidized_newmms), np.mean(py_newmms) / np.mean(oxidized_newmms)"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 16,
-   "metadata": {
-    "id": "8hRoDxm966Fp",
-    "outputId": "c4e8c0fd-97ca-4e3a-ee63-1281f84bb1d9"
-   },
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "(array([1.5343e+04, 3.2270e+03, 1.2610e+03, 7.7200e+02, 3.3000e+02,\n",
-       "        2.2600e+02, 1.1900e+02, 7.8000e+01, 6.1000e+01, 5.3000e+01,\n",
-       "        3.4000e+01, 2.8000e+01, 1.2000e+01, 1.0000e+01, 1.1000e+01,\n",
-       "        9.0000e+00, 1.0000e+01, 1.2000e+01, 9.0000e+00, 4.0000e+00,\n",
-       "        5.0000e+00, 4.0000e+00, 5.0000e+00, 1.0000e+00, 0.0000e+00,\n",
-       "        3.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 1.0000e+00]),\n",
-       " array([4.76837158e-06, 1.00572904e-04, 1.96377436e-04, 2.92181969e-04,\n",
-       "        3.87986501e-04, 4.83791033e-04, 5.79595566e-04, 6.75400098e-04,\n",
-       "        7.71204631e-04, 8.67009163e-04, 9.62813695e-04, 1.05861823e-03,\n",
-       "        1.15442276e-03, 1.25022729e-03, 1.34603182e-03, 1.44183636e-03,\n",
-       "        1.53764089e-03, 1.63344542e-03, 1.72924995e-03, 1.82505449e-03,\n",
-       "        1.92085902e-03, 2.01666355e-03, 2.11246808e-03, 2.20827262e-03,\n",
-       "        2.30407715e-03, 2.39988168e-03, 2.49568621e-03, 2.59149075e-03,\n",
-       "        2.68729528e-03, 2.78309981e-03, 2.87890434e-03]),\n",
-       " <BarContainer object of 30 artists>)"
-      ]
-     },
-     "execution_count": 16,
-     "metadata": {},
-     "output_type": "execute_result"
-    },
-    {
-     "data": {
-      "image/png": "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\n",
-      "text/plain": [
-       "<Figure size 432x288 with 1 Axes>"
-      ]
-     },
-     "metadata": {
-      "needs_background": "light"
-     },
-     "output_type": "display_data"
-    }
-   ],
-   "source": [
-    "# look at distribution; oxidized newmm also consistently performs better\n",
-    "plt.hist(py_newmms, bins=30, alpha=0.5)\n",
-    "plt.hist(oxidized_newmms, bins=30, alpha=0.5)"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {
-    "id": "EMZZ8SgY66Fp"
-   },
-   "source": [
-    "## Run Time as Sequence Length Grows"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 17,
-   "metadata": {
-    "id": "3P_z59rS66Fp",
-    "outputId": "5951dd75-388c-4f9e-a1df-f0e98f663ec3"
-   },
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "('เปิดศักราชใหม่! นายกฯ แถลงข่าวก่อนการแข่งขัน', 10)"
-      ]
-     },
-     "execution_count": 17,
-     "metadata": {},
-     "output_type": "execute_result"
-    }
-   ],
-   "source": [
-    "txt = datasets['train']['texts'][1]\n",
-    "txt2 = ''.join(oxidized_newmm(txt)[:10])\n",
-    "txt2, len(oxidized_newmm(txt2))"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 18,
-   "metadata": {
-    "colab": {
-     "referenced_widgets": [
-      "0689a5cf946049a0ac98bdf9e1353810",
-      "2b481450056f4c1883c163bf066110a3"
-     ]
-    },
-    "id": "FEmDkPHL66Fq",
-    "outputId": "2b2c96ab-7044-423b-9b40-e06ca186a213"
-   },
-   "outputs": [
-    {
-     "data": {
-      "application/vnd.jupyter.widget-view+json": {
-       "model_id": "422dcd3924314206b11d7cf57fc81c84",
-       "version_major": 2,
-       "version_minor": 0
-      },
-      "text/plain": [
-       "HBox(children=(FloatProgress(value=0.0, max=5.0), HTML(value='')))"
-      ]
-     },
-     "metadata": {},
-     "output_type": "display_data"
-    },
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "\n"
-     ]
-    },
-    {
-     "data": {
-      "application/vnd.jupyter.widget-view+json": {
-       "model_id": "7743e037486e4d11ba020835e3db335e",
-       "version_major": 2,
-       "version_minor": 0
-      },
-      "text/plain": [
-       "HBox(children=(FloatProgress(value=0.0, max=5.0), HTML(value='')))"
-      ]
-     },
-     "metadata": {},
-     "output_type": "display_data"
-    },
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "\n"
-     ]
-    }
-   ],
-   "source": [
-    "py_newmms = [time_func(py_newmm, txt2*i) for i in tqdm([10**j for j in range(5)])]\n",
-    "oxidized_newmms = [time_func(oxidized_newmm, txt2*i) for i in tqdm([10**j for j in range(5)])]"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "Performance starts really deviate when sequence length > 10^3 tokens and above.\n",
-    "\n",
-    "python newmm is dashed line."
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 19,
-   "metadata": {
-    "id": "sT8GL0oX66Fr",
-    "outputId": "6bb4acfd-4721-47cb-d8ff-943c67a4cedf"
-   },
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "[<matplotlib.lines.Line2D at 0x13343f4f0>]"
-      ]
-     },
-     "execution_count": 19,
-     "metadata": {},
-     "output_type": "execute_result"
-    },
-    {
-     "data": {
-      "image/png": "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\n",
-      "text/plain": [
-       "<Figure size 432x288 with 1 Axes>"
-      ]
-     },
-     "metadata": {
-      "needs_background": "light"
-     },
-     "output_type": "display_data"
-    }
-   ],
-   "source": [
-    "positions = [i for i in range(5)]\n",
-    "labels = [f'10^{i+1}' for i in range(5)]\n",
-    "plt.xticks(positions, labels)\n",
-    "plt.plot(py_newmms, linestyle='dashed')\n",
-    "plt.plot(oxidized_newmms)"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 20,
-   "metadata": {
-    "id": "5YU5aiNs66Fs"
-   },
-   "outputs": [
-    {
-     "data": {
-      "text/plain": [
-       "[<matplotlib.lines.Line2D at 0x13319f7c0>]"
-      ]
-     },
-     "execution_count": 20,
-     "metadata": {},
-     "output_type": "execute_result"
-    },
-    {
-     "data": {
-      "image/png": "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\n",
-      "text/plain": [
-       "<Figure size 432x288 with 1 Axes>"
-      ]
-     },
-     "metadata": {
-      "needs_background": "light"
-     },
-     "output_type": "display_data"
-    }
-   ],
-   "source": [
-    "# zooming in on inflexion point\n",
-    "positions = [i for i in range(4)]\n",
-    "labels = [f'10^{i+1}' for i in range(4)]\n",
-    "plt.xticks(positions, labels)\n",
-    "plt.plot(py_newmms[:-1], linestyle='dashed')\n",
-    "plt.plot(oxidized_newmms[:-1])"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": null,
-   "metadata": {},
-   "outputs": [],
-   "source": []
-  }
- ],
- "metadata": {
-  "colab": {
-   "name": "oxidized_segment.ipynb",
-   "provenance": []
-  },
-  "kernelspec": {
-   "display_name": "Python 3",
-   "language": "python",
-   "name": "python3"
-  },
-  "language_info": {
-   "codemirror_mode": {
-    "name": "ipython",
-    "version": 3
-   },
-   "file_extension": ".py",
-   "mimetype": "text/x-python",
-   "name": "python",
-   "nbconvert_exporter": "python",
-   "pygments_lexer": "ipython3",
-   "version": "3.9.5"
-  }
- },
- "nbformat": 4,
- "nbformat_minor": 1
-}
diff --git a/python_binding/Cargo.toml b/python_binding/Cargo.toml
deleted file mode 100644
index a4b3126..0000000
--- a/python_binding/Cargo.toml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-[package]
-name = "oxidized-thainlp"
-version = "1.1.0"
-edition = "2018"
-authors = ["Thanathip Suntorntip Gorlph"]
-
-[lib]
-name = "oxidized_thainlp"
-path = "src/lib.rs"# "cdylib" is necessary to produce a shared library for Python to import from.
-#
-# Downstream Rust code (including code in `bin/`, `examples/`, and `tests/`) will not be able
-# to `use string_sum;` unless the "rlib" or "lib" crate type is also included, e.g.:
-# crate-type = ["cdylib", "rlib"]
-crate-type = ["cdylib", "rlib"]
-
-[dependencies.pyo3]
-version = "0.13.2"
-features = ["extension-module"]
-
-[dependencies]
-lazy_static = "1.4.0"
-ahash = "0.7.2"
-pythainlp-rust-modules = { path = ".." }
\ No newline at end of file
diff --git a/python_binding/pyproject.toml b/python_binding/pyproject.toml
deleted file mode 100644
index b62e759..0000000
--- a/python_binding/pyproject.toml
+++ /dev/null
@@ -1,36 +0,0 @@
-[build-system]
-requires = ["maturin>=0.10,<0.11"]
-build-backend = "maturin"
-
-[tool.poetry]
-name = "pythainlp-rust-modules"
-version = "1.0.0"
-authors = [
-    "Thanathip Suntorntip",
-    "Arthit Suriyawongkul",
-    "Wannaphong Phatthiyaphaibun",
-]
-description = "Thai Natural Language Processing in Rust, with Python-binding."
-readme = "README.md"
-repository = "https://github.com/PyThaiNLP/oxidized-thainlp"
-homepage = "https://github.com/PyThaiNLP/oxidized-thainlp"
-license = "Apache-2.0"
-keywords = ["thai", "tokenizer", "nlp", "rust", "pyo3"]
-classifiers = [
-    "License :: OSI Approved :: Apache Software License",
-    "Programming Language :: Python :: 3",
-    "Programming Language :: Rust",
-    "Natural Language :: Thai",
-    "Topic :: Scientific/Engineering :: Artificial Intelligence",
-    "Topic :: Text Processing :: Linguistic",
-    "Topic :: Software Development :: Libraries :: Python Modules",
-]
-
-[tool.poetry.dependencies]
-python = "^3.6"
-
-[tool.poetry.dev-dependencies]
-pytest = "*"
-wheel = "*"
-pytest-runner = "*"
-maturin = "*"
diff --git a/src/tokenizer/tcc_custom.rs b/src/tokenizer/tcc_custom.rs
index c180ff8..b8a4b25 100644
--- a/src/tokenizer/tcc_custom.rs
+++ b/src/tokenizer/tcc_custom.rs
@@ -22,7 +22,7 @@ use super::super::fixed_bytes_str::four_bytes::BYTES_PER_CHAR;
 // \x00 is byte value 0, every unicode character in regex is padded with \x00 to 4 bytes length
 // https://www.fileformat.info/info/unicode/
 // Thai characters use 3 bytes per character, so it is padded with \x00 only once.
-// The following regexpressions are translated from pythainlp/tokeniz/tcc.py
+// The following regexpressions are translated from pythainlp/tokenize/tcc.py
 lazy_static! {
     static ref NON_LOOKAHEAD_TCC: Regex = Regex::new(
         &[


Test patch:
diff --git a/.github/workflows/test-main-lib.yml b/.github/workflows/test-main-lib.yml
new file mode 100644
index 0000000..811588e
--- /dev/null
+++ b/.github/workflows/test-main-lib.yml
@@ -0,0 +1,57 @@
+name: Test main lib (Rust crate)
+on: [push, pull_request]
+
+jobs:
+  test:
+    runs-on: ${{ matrix.os }}
+
+    strategy:
+      fail-fast: false
+      matrix:
+        os: [macos-latest, ubuntu-latest, windows-latest]
+        bitness: [64]  # 32, 64
+        include:
+          - os: windows-latest
+            bitness: 32
+
+    steps:
+      - name: Checkout source code
+        uses: actions/checkout@master
+
+      - name: Setup Rust toolchain - non-win32
+        uses: actions-rs/toolchain@v1
+        with:
+          override: true
+          profile: minimal
+          toolchain: stable
+        if: ${{ !startsWith(matrix.os, 'windows') || matrix.bitness != '32' }}
+
+      - name: Setup Rust toolchain - win32
+        uses: actions-rs/toolchain@v1
+        with:
+          override: true
+          profile: minimal
+          toolchain: stable
+          target: i686-pc-windows-msvc
+        if: startsWith(matrix.os, 'windows') && matrix.bitness == '32'
+
+      - name: Setup Rust dependencies
+        uses: actions-rs/cargo@v1
+        with:
+          command: check
+
+      - name: Cache Rust files
+        uses: actions/cache@v2
+        with:
+          path: |
+            ~/.cargo/bin/
+            ~/.cargo/registry/index/
+            ~/.cargo/registry/cache/
+            ~/.cargo/git/db/
+            target/
+          key: ${{ matrix.os }}-${{ matrix.bitness }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+          restore-keys: |
+            ${{ matrix.os }}-${{ matrix.bitness }}-cargo-
+
+      - name: Test
+        run: cargo test
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
deleted file mode 100644
index 73f9129..0000000
--- a/.github/workflows/test.yml
+++ /dev/null
@@ -1,49 +0,0 @@
-name: CI
-on: [push, pull_request]
-
-jobs:
-  test:
-    runs-on: ${{ matrix.os }}
-    strategy:
-      matrix:
-        os: [ubuntu-latest, windows-latest] # , macos-latest, windows-latest
-        python-version: [3.6, 3.7, 3.8]
-        architecture: ['x64']
-    steps:
-      - uses: actions/checkout@master
-      - name: Setup python
-        uses: actions/setup-python@v2
-        with:
-          python-version: ${{ matrix.python-version }}
-
-      - uses: actions-rs/toolchain@v1
-        with:
-          profile: minimal
-          toolchain: stable
-          override: true
-
-      # Install dependencies on ubuntu
-      - if: matrix.os == 'ubuntu-latest'
-        name: Install build dependencies
-        run: sudo apt-get install -y libxml2-dev libxslt-dev
-
-      # Install 32-bit windows target for pypy3
-      - name: Install
-        run: pip install maturin 
-   
-      - if: matrix.os == 'windows-latest' && matrix.python-version == 'pypy3'
-        name: Install 32-bit Rust target
-        uses: actions-rs/toolchain@v1
-        with:
-          profile: minimal
-          toolchain: stable
-          override: true
-          target: i686-pc-windows-msvc
-      - if: matrix.os == 'windows-latest'
-        name: Install beta Maturin for windows
-        run: pip install --pre -U maturin
-
-      - name: Build package   
-        run: maturin build --release -i python -m python_binding/Cargo.toml
-      - name: Test
-        run: cargo test
diff --git a/tests/test_custom_string.rs b/tests/test_custom_string.rs
index 10a4df8..6edb962 100644
--- a/tests/test_custom_string.rs
+++ b/tests/test_custom_string.rs
@@ -1,4 +1,4 @@
-use oxidized_thainlp_main_lib::fixed_bytes_str::four_bytes::CustomString;
+use nlpo3::fixed_bytes_str::four_bytes::CustomString;
 
 // const short_text_1: &str = "หมอนทองตากลมหูว์MBK39 :.ฉฺ๐๐๓-#™±";
 // const short_text_2: &str = "ทดสอบ";
diff --git a/tests/test_tokenizer.rs b/tests/test_tokenizer.rs
index fba5a9e..8eb9cb9 100644
--- a/tests/test_tokenizer.rs
+++ b/tests/test_tokenizer.rs
@@ -1,5 +1,5 @@
-use oxidized_thainlp_main_lib::tokenizer::tokenizer_trait::Tokenizer;
-use oxidized_thainlp_main_lib::tokenizer::{
+use nlpo3::tokenizer::tokenizer_trait::Tokenizer;
+use nlpo3::tokenizer::{
     //dict_reader_custom::{
     //    create_dict_trie as create_custom_dict_trie, DictSource as CustomDictSource,
     //},