Upload folder using huggingface_hub
Browse files- .gitattributes +1 -0
- cpp/demo +0 -0
- cpp/libsherpa_punct.a +0 -0
- model.axmodel +3 -0
- python/example.py +99 -0
- python/requirements.txt +5 -0
- python/sherpa_punct_sdk/__init__.py +17 -0
- python/sherpa_punct_sdk/inference.py +62 -0
- python/sherpa_punct_sdk/pipeline.py +93 -0
- python/sherpa_punct_sdk/postprocess.py +102 -0
- python/sherpa_punct_sdk/preprocess.py +128 -0
- tokens.json +0 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
*.axmodel filter=lfs diff=lfs merge=lfs -text
|
cpp/demo
ADDED
|
Binary file (50.9 kB). View file
|
|
|
cpp/libsherpa_punct.a
ADDED
|
Binary file (40.9 kB). View file
|
|
|
model.axmodel
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:bdc5e8051c89961b4212929ddf129b9e8976c04ec55000a7c9b72c66678e4886
|
| 3 |
+
size 283103944
|
python/example.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Sherpa Punctuation Prediction on AX650 NPU.
|
| 3 |
+
|
| 4 |
+
Usage:
|
| 5 |
+
python example.py # run demo texts
|
| 6 |
+
python example.py "今天天气真好我们出去散步吧" # custom text
|
| 7 |
+
python example.py -m /path/to/model.axmodel # specify model
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import argparse
|
| 11 |
+
import os
|
| 12 |
+
import sys
|
| 13 |
+
|
| 14 |
+
from sherpa_punct_sdk import PunctuationPipeline
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
DEMO_TEXTS = [
|
| 18 |
+
"你好吗how are you我很好谢谢",
|
| 19 |
+
"今天天气真不错我们出去走走吧",
|
| 20 |
+
"这个方案有三个优点第一成本低第二效率高第三维护简单",
|
| 21 |
+
"请确认以下事项一合同已签署二款项已到账三交付日期已确定",
|
| 22 |
+
# Long text (>64 tokens): auto-sliding-window test
|
| 23 |
+
"人工智能技术正在改变我们的生活方式"
|
| 24 |
+
"明天下午三点在公司会议室开会请准时参加"
|
| 25 |
+
"他是一名优秀的工程师工作认真负责"
|
| 26 |
+
"北京是中国的首都拥有悠久的历史文化"
|
| 27 |
+
"随着科技的发展人们的生活越来越便利",
|
| 28 |
+
]
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def find_file(*candidates):
|
| 32 |
+
"""Return the first existing file from candidates."""
|
| 33 |
+
for path in candidates:
|
| 34 |
+
if os.path.exists(path):
|
| 35 |
+
return path
|
| 36 |
+
return None
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def resolve_paths(args):
|
| 40 |
+
"""Resolve model and tokens paths with smart defaults."""
|
| 41 |
+
base = os.path.dirname(os.path.abspath(__file__))
|
| 42 |
+
project = os.path.dirname(base)
|
| 43 |
+
|
| 44 |
+
model = args.model or find_file(
|
| 45 |
+
os.path.join(project, "model.axmodel"),
|
| 46 |
+
os.path.join(project, "models", "model.axmodel"),
|
| 47 |
+
os.path.join(project, "model_convert", "compile", "model.axmodel"),
|
| 48 |
+
)
|
| 49 |
+
tokens = args.tokens or find_file(
|
| 50 |
+
os.path.join(project, "tokens.json"),
|
| 51 |
+
os.path.join(project, "models", "tokens.json"),
|
| 52 |
+
os.path.join(project, "model_convert", "export", "tokens.json"),
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
if not model:
|
| 56 |
+
print("ERROR: model.axmodel not found. Use -m to specify path.")
|
| 57 |
+
sys.exit(1)
|
| 58 |
+
if not tokens:
|
| 59 |
+
print("ERROR: tokens.json not found. Use -t to specify path.")
|
| 60 |
+
sys.exit(1)
|
| 61 |
+
|
| 62 |
+
return model, tokens
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def main():
|
| 66 |
+
parser = argparse.ArgumentParser(
|
| 67 |
+
description="Sherpa Punctuation Prediction on AX650 NPU",
|
| 68 |
+
)
|
| 69 |
+
parser.add_argument(
|
| 70 |
+
"text", nargs="*",
|
| 71 |
+
help="Text to punctuate. If omitted, runs demo texts.",
|
| 72 |
+
)
|
| 73 |
+
parser.add_argument(
|
| 74 |
+
"-m", "--model",
|
| 75 |
+
help="Path to model.axmodel (default: auto-detect)",
|
| 76 |
+
)
|
| 77 |
+
parser.add_argument(
|
| 78 |
+
"-t", "--tokens",
|
| 79 |
+
help="Path to tokens.json (default: auto-detect)",
|
| 80 |
+
)
|
| 81 |
+
args = parser.parse_args()
|
| 82 |
+
|
| 83 |
+
model_path, tokens_path = resolve_paths(args)
|
| 84 |
+
|
| 85 |
+
print(f"Model: {model_path}")
|
| 86 |
+
print(f"Tokens: {tokens_path}")
|
| 87 |
+
|
| 88 |
+
pipeline = PunctuationPipeline(model_path, tokens_path)
|
| 89 |
+
|
| 90 |
+
texts = args.text if args.text else DEMO_TEXTS
|
| 91 |
+
|
| 92 |
+
for text in texts:
|
| 93 |
+
result = pipeline(text)
|
| 94 |
+
print(f"\nInput: {text}")
|
| 95 |
+
print(f"Output: {result}")
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
if __name__ == "__main__":
|
| 99 |
+
main()
|
python/requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
numpy>=1.21
|
| 2 |
+
# On-device AX650 NPU inference:
|
| 3 |
+
# git clone https://github.com/AXERA-TECH/pyaxengine.git
|
| 4 |
+
# cd pyaxengine && pip install .
|
| 5 |
+
# pyaxengine
|
python/sherpa_punct_sdk/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Sherpa Punctuation SDK for AX650
|
| 2 |
+
#
|
| 3 |
+
# Converts raw Chinese text to punctuation-annotated text using
|
| 4 |
+
# the compiled AXMODEL on AXera NPU (or CPU fallback).
|
| 5 |
+
|
| 6 |
+
from .pipeline import PunctuationPipeline
|
| 7 |
+
from .preprocess import CharTokenizer
|
| 8 |
+
from .postprocess import decode_punctuation
|
| 9 |
+
from .inference import PunctInference
|
| 10 |
+
|
| 11 |
+
__all__ = [
|
| 12 |
+
"PunctuationPipeline",
|
| 13 |
+
"CharTokenizer",
|
| 14 |
+
"decode_punctuation",
|
| 15 |
+
"PunctInference",
|
| 16 |
+
]
|
| 17 |
+
__version__ = "1.0.0"
|
python/sherpa_punct_sdk/inference.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Sherpa Punctuation Inference Engine
|
| 2 |
+
#
|
| 3 |
+
# Uses pyaxengine's AxEngineExecutionProvider to run the compiled AXMODEL
|
| 4 |
+
# on AX650 NPU.
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
from typing import Optional
|
| 8 |
+
|
| 9 |
+
import numpy as np
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class PunctInference:
|
| 13 |
+
"""Inference wrapper for sherpa punct CT Transformer AXMODEL."""
|
| 14 |
+
|
| 15 |
+
def __init__(
|
| 16 |
+
self,
|
| 17 |
+
model_path: str,
|
| 18 |
+
provider: Optional[str] = None,
|
| 19 |
+
):
|
| 20 |
+
"""Initialize the inference engine.
|
| 21 |
+
|
| 22 |
+
Args:
|
| 23 |
+
model_path: Path to compiled model.axmodel.
|
| 24 |
+
provider: Execution provider (default: AxEngineExecutionProvider).
|
| 25 |
+
"""
|
| 26 |
+
if not os.path.exists(model_path):
|
| 27 |
+
raise FileNotFoundError(f"Model not found: {model_path}")
|
| 28 |
+
|
| 29 |
+
self.model_path = model_path
|
| 30 |
+
self.provider = provider or "AxEngineExecutionProvider"
|
| 31 |
+
self._session = None
|
| 32 |
+
|
| 33 |
+
def _create_session(self):
|
| 34 |
+
"""Create AX Engine inference session."""
|
| 35 |
+
import axengine
|
| 36 |
+
|
| 37 |
+
available = axengine.get_available_providers()
|
| 38 |
+
if self.provider in available:
|
| 39 |
+
return axengine.InferenceSession(
|
| 40 |
+
self.model_path,
|
| 41 |
+
providers=[self.provider],
|
| 42 |
+
)
|
| 43 |
+
return axengine.InferenceSession(
|
| 44 |
+
self.model_path,
|
| 45 |
+
providers=available,
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
def __call__(self, inputs: np.ndarray) -> np.ndarray:
|
| 49 |
+
"""Run inference.
|
| 50 |
+
|
| 51 |
+
Args:
|
| 52 |
+
inputs: (1, 64) int32 numpy array of token IDs.
|
| 53 |
+
|
| 54 |
+
Returns:
|
| 55 |
+
logits: (1, 64, 6) float32 numpy array.
|
| 56 |
+
"""
|
| 57 |
+
if self._session is None:
|
| 58 |
+
self._session = self._create_session()
|
| 59 |
+
|
| 60 |
+
input_name = self._session.get_inputs()[0].name
|
| 61 |
+
results = self._session.run(None, {input_name: inputs})
|
| 62 |
+
return results[0]
|
python/sherpa_punct_sdk/pipeline.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Sherpa Onnx Punctuation Pipeline
|
| 2 |
+
#
|
| 3 |
+
# End-to-end pipeline: text → tokens → inference → punctuation-annotated text.
|
| 4 |
+
# Long text is automatically split into overlapping windows for the model's
|
| 5 |
+
# fixed 64-token input.
|
| 6 |
+
|
| 7 |
+
from typing import List
|
| 8 |
+
|
| 9 |
+
import numpy as np
|
| 10 |
+
|
| 11 |
+
from .preprocess import CharTokenizer
|
| 12 |
+
from .inference import PunctInference
|
| 13 |
+
from .postprocess import decode_punctuation
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
INPUT_LENGTH = 64
|
| 17 |
+
WINDOW_STRIDE = 60 # step size; overlap = INPUT_LENGTH - WINDOW_STRIDE = 4
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class PunctuationPipeline:
|
| 21 |
+
"""End-to-end punctuation prediction pipeline.
|
| 22 |
+
|
| 23 |
+
Usage:
|
| 24 |
+
pipeline = PunctuationPipeline("model.axmodel", "tokens.json")
|
| 25 |
+
result = pipeline("你好吗how are you我很好谢谢")
|
| 26 |
+
print(result) # 你好吗,how are you,我很好谢谢。
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
def __init__(
|
| 30 |
+
self,
|
| 31 |
+
model_path: str,
|
| 32 |
+
tokens_path: str,
|
| 33 |
+
provider: str = "AxEngineExecutionProvider",
|
| 34 |
+
):
|
| 35 |
+
self.tokenizer = CharTokenizer(tokens_path)
|
| 36 |
+
if not hasattr(self.tokenizer, "id2token") or not self.tokenizer.id2token:
|
| 37 |
+
raise RuntimeError("Failed to load tokens.json")
|
| 38 |
+
self.id2token = self.tokenizer.id2token
|
| 39 |
+
self.inference = PunctInference(model_path, provider)
|
| 40 |
+
|
| 41 |
+
def _run_window(self, tokens: list[int]) -> np.ndarray:
|
| 42 |
+
"""Run inference on a single window, return logits for valid tokens."""
|
| 43 |
+
n = len(tokens)
|
| 44 |
+
padded = np.zeros((1, INPUT_LENGTH), dtype=np.int32)
|
| 45 |
+
padded[0, :n] = tokens
|
| 46 |
+
logits = self.inference(padded) # (1, INPUT_LENGTH, 6)
|
| 47 |
+
return logits[0, :n, :] # only valid token positions
|
| 48 |
+
|
| 49 |
+
def __call__(self, text: str) -> str:
|
| 50 |
+
"""Add punctuation to input text.
|
| 51 |
+
|
| 52 |
+
Long text (>64 tokens) is processed in overlapping windows:
|
| 53 |
+
window_size=64, stride=60, overlap=4.
|
| 54 |
+
|
| 55 |
+
Args:
|
| 56 |
+
text: Raw Chinese text (may include English words).
|
| 57 |
+
|
| 58 |
+
Returns:
|
| 59 |
+
Punctuation-annotated text.
|
| 60 |
+
"""
|
| 61 |
+
token_ids = self.tokenizer.tokenize(text)
|
| 62 |
+
if not token_ids:
|
| 63 |
+
return text
|
| 64 |
+
|
| 65 |
+
# Short text: single inference
|
| 66 |
+
if len(token_ids) <= INPUT_LENGTH:
|
| 67 |
+
logits = self._run_window(token_ids)
|
| 68 |
+
return decode_punctuation(
|
| 69 |
+
logits[np.newaxis], token_ids, self.id2token, len(token_ids),
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
# Long text: sliding window
|
| 73 |
+
all_logits = []
|
| 74 |
+
for start in range(0, len(token_ids), WINDOW_STRIDE):
|
| 75 |
+
end = min(start + INPUT_LENGTH, len(token_ids))
|
| 76 |
+
window_tokens = token_ids[start:end]
|
| 77 |
+
|
| 78 |
+
logits = self._run_window(window_tokens) # (end-start, 6)
|
| 79 |
+
|
| 80 |
+
if start == 0:
|
| 81 |
+
all_logits.append(logits)
|
| 82 |
+
else:
|
| 83 |
+
# Discard overlap: previous window already covered those tokens
|
| 84 |
+
overlap = INPUT_LENGTH - WINDOW_STRIDE
|
| 85 |
+
new_tokens_start = overlap
|
| 86 |
+
all_logits.append(logits[new_tokens_start:])
|
| 87 |
+
|
| 88 |
+
combined = np.concatenate(all_logits, axis=0)[:len(token_ids)]
|
| 89 |
+
combined = combined[np.newaxis, :, :] # (1, N, 6)
|
| 90 |
+
|
| 91 |
+
return decode_punctuation(
|
| 92 |
+
combined, token_ids, self.id2token, len(token_ids),
|
| 93 |
+
)
|
python/sherpa_punct_sdk/postprocess.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Sherpa Onnx Punctuation Postprocessor
|
| 2 |
+
#
|
| 3 |
+
# Converts model logits output to punctuation-annotated text.
|
| 4 |
+
# 6 classes: <unk>(0), _(1), ,(2), 。(3), ?(4), 、(5)
|
| 5 |
+
|
| 6 |
+
import numpy as np
|
| 7 |
+
from typing import List
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
PUNCT_CLASSES = [0, 1, 2, 3, 4, 5]
|
| 11 |
+
PUNCT_MARKS = ["", "", ",", "。", "?", "、"]
|
| 12 |
+
IGNORE_ID = 1 # underscore class = no punctuation
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def decode_punctuation(
|
| 16 |
+
logits: np.ndarray,
|
| 17 |
+
token_ids: List[int],
|
| 18 |
+
id2token: List[str],
|
| 19 |
+
original_length: int,
|
| 20 |
+
dot_id: int = 3,
|
| 21 |
+
comma_id: int = 2,
|
| 22 |
+
quest_id: int = 4,
|
| 23 |
+
pause_id: int = 5,
|
| 24 |
+
) -> str:
|
| 25 |
+
"""Decode the model output to punctuation-annotated text.
|
| 26 |
+
|
| 27 |
+
Args:
|
| 28 |
+
logits: (1, 64, 6) float32 array from model
|
| 29 |
+
token_ids: list of original (unpadded) token IDs
|
| 30 |
+
id2token: vocab list mapping ID → token string
|
| 31 |
+
original_length: length before padding (<= 64)
|
| 32 |
+
dot_id, comma_id, quest_id, pause_id: class IDs for punctuation
|
| 33 |
+
|
| 34 |
+
Returns:
|
| 35 |
+
Annotated text string with punctuation inserted
|
| 36 |
+
"""
|
| 37 |
+
# Take only valid portion
|
| 38 |
+
if original_length > logits.shape[1]:
|
| 39 |
+
original_length = logits.shape[1]
|
| 40 |
+
if original_length > len(token_ids):
|
| 41 |
+
original_length = len(token_ids)
|
| 42 |
+
|
| 43 |
+
logits = logits[0, :original_length, :]
|
| 44 |
+
ids = token_ids[:original_length]
|
| 45 |
+
|
| 46 |
+
# Argmax over classes
|
| 47 |
+
out = np.argmax(logits, axis=-1).tolist()
|
| 48 |
+
|
| 49 |
+
# Segment with sentence-boundary heuristics
|
| 50 |
+
# (simplified from original sherpa code)
|
| 51 |
+
max_len = 200
|
| 52 |
+
segment_size = 20
|
| 53 |
+
num_segments = (len(ids) + segment_size - 1) // segment_size
|
| 54 |
+
|
| 55 |
+
punctuations = []
|
| 56 |
+
last = -1
|
| 57 |
+
for i in range(num_segments):
|
| 58 |
+
this_start = i * segment_size
|
| 59 |
+
this_end = min(this_start + segment_size, len(ids))
|
| 60 |
+
if last != -1:
|
| 61 |
+
this_start = last
|
| 62 |
+
|
| 63 |
+
seg_out = out[this_start:this_end]
|
| 64 |
+
|
| 65 |
+
dot_index = -1
|
| 66 |
+
comma_index = -1
|
| 67 |
+
for k in range(len(seg_out) - 1, 1, -1):
|
| 68 |
+
if seg_out[k] in (dot_id, quest_id):
|
| 69 |
+
dot_index = k
|
| 70 |
+
break
|
| 71 |
+
if comma_index == -1 and seg_out[k] == comma_id:
|
| 72 |
+
comma_index = k
|
| 73 |
+
|
| 74 |
+
if dot_index == -1 and len(ids) >= max_len and comma_index != -1:
|
| 75 |
+
dot_index = comma_index
|
| 76 |
+
seg_out[dot_index] = dot_id
|
| 77 |
+
|
| 78 |
+
if dot_index == -1:
|
| 79 |
+
if last == -1:
|
| 80 |
+
last = this_start
|
| 81 |
+
if i == num_segments - 1:
|
| 82 |
+
dot_index = len(seg_out) - 1
|
| 83 |
+
else:
|
| 84 |
+
last = this_start + dot_index + 1
|
| 85 |
+
|
| 86 |
+
if dot_index != -1:
|
| 87 |
+
punctuations += seg_out[: dot_index + 1]
|
| 88 |
+
|
| 89 |
+
# Build output
|
| 90 |
+
ans = []
|
| 91 |
+
for j, p in enumerate(punctuations):
|
| 92 |
+
if j >= len(ids):
|
| 93 |
+
break
|
| 94 |
+
t = id2token[ids[j]] if ids[j] < len(id2token) else "<unk>"
|
| 95 |
+
# Insert space before ASCII tokens
|
| 96 |
+
if ans and len(ans[-1][0].encode()) == 1 and len(t[0].encode()) == 1:
|
| 97 |
+
ans.append(" ")
|
| 98 |
+
ans.append(t)
|
| 99 |
+
if p != IGNORE_ID and p < len(PUNCT_MARKS) and PUNCT_MARKS[p]:
|
| 100 |
+
ans.append(PUNCT_MARKS[p])
|
| 101 |
+
|
| 102 |
+
return "".join(ans)
|
python/sherpa_punct_sdk/preprocess.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Sherpa Onnx Punctuation Preprocessor (CharTokenizer)
|
| 2 |
+
#
|
| 3 |
+
# Model: sherpa-onnx-punct-ct-transformer
|
| 4 |
+
# Tokenizer: character-level for Chinese, word-level for English
|
| 5 |
+
# Vocab: tokens.json (272727 entries)
|
| 6 |
+
# Padding: to 64 tokens
|
| 7 |
+
|
| 8 |
+
import json
|
| 9 |
+
import os
|
| 10 |
+
from typing import List, Tuple
|
| 11 |
+
import numpy as np
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class CharTokenizer:
|
| 15 |
+
"""Character/word tokenizer for the sherpa punct CT Transformer model."""
|
| 16 |
+
|
| 17 |
+
def __init__(self, tokens_path: str, unk_symbol: str = "<unk>"):
|
| 18 |
+
if not os.path.exists(tokens_path):
|
| 19 |
+
raise FileNotFoundError(f"tokens.json not found: {tokens_path}")
|
| 20 |
+
with open(tokens_path, "r", encoding="utf-8") as f:
|
| 21 |
+
id2token = json.load(f)
|
| 22 |
+
self.id2token = id2token
|
| 23 |
+
self.token2id = {tok: idx for idx, tok in enumerate(id2token)}
|
| 24 |
+
self.unk_id = self.token2id.get(unk_symbol, 0)
|
| 25 |
+
|
| 26 |
+
def tokenize(self, text: str) -> List[int]:
|
| 27 |
+
"""Split text into tokens and return token IDs.
|
| 28 |
+
|
| 29 |
+
Chinese characters are segmented individually.
|
| 30 |
+
English words are kept as whole tokens.
|
| 31 |
+
"""
|
| 32 |
+
# Split on whitespace
|
| 33 |
+
word_list = text.split()
|
| 34 |
+
|
| 35 |
+
words = []
|
| 36 |
+
for w in word_list:
|
| 37 |
+
s = ""
|
| 38 |
+
for c in w:
|
| 39 |
+
if len(c.encode()) > 1:
|
| 40 |
+
# Multi-byte character (Chinese, Japanese, etc.)
|
| 41 |
+
if s == "":
|
| 42 |
+
s = c
|
| 43 |
+
elif len(s[-1].encode()) > 1:
|
| 44 |
+
s += c
|
| 45 |
+
else:
|
| 46 |
+
words.append(s)
|
| 47 |
+
s = c
|
| 48 |
+
else:
|
| 49 |
+
# ASCII character
|
| 50 |
+
if s == "":
|
| 51 |
+
s = c
|
| 52 |
+
elif len(s[-1].encode()) > 1:
|
| 53 |
+
words.append(s)
|
| 54 |
+
s = c
|
| 55 |
+
else:
|
| 56 |
+
s += c
|
| 57 |
+
if s:
|
| 58 |
+
words.append(s)
|
| 59 |
+
|
| 60 |
+
ids = []
|
| 61 |
+
for w in words:
|
| 62 |
+
if len(w[0].encode()) > 1:
|
| 63 |
+
# Chinese phrase: tokenize each character
|
| 64 |
+
for c in w:
|
| 65 |
+
ids.append(self.token2id.get(c, self.unk_id))
|
| 66 |
+
else:
|
| 67 |
+
ids.append(self.token2id.get(w, self.unk_id))
|
| 68 |
+
return ids
|
| 69 |
+
|
| 70 |
+
def tokenize_full(self, text: str) -> List[int]:
|
| 71 |
+
"""Tokenize full text without truncation or padding."""
|
| 72 |
+
return self.tokenize(text)
|
| 73 |
+
|
| 74 |
+
def encode(
|
| 75 |
+
self, text: str, pad_length: int = 64
|
| 76 |
+
) -> Tuple[np.ndarray, int]:
|
| 77 |
+
"""Tokenize and pad to fixed length. Truncates if > pad_length.
|
| 78 |
+
|
| 79 |
+
Returns:
|
| 80 |
+
input_array: (1, pad_length) int32 numpy array
|
| 81 |
+
original_length: actual token count before padding
|
| 82 |
+
"""
|
| 83 |
+
ids = self.tokenize(text)
|
| 84 |
+
original_len = len(ids)
|
| 85 |
+
|
| 86 |
+
# Truncate or pad to pad_length
|
| 87 |
+
if len(ids) > pad_length:
|
| 88 |
+
ids = ids[:pad_length]
|
| 89 |
+
original_len = pad_length
|
| 90 |
+
|
| 91 |
+
padded = np.zeros((1, pad_length), dtype=np.int32)
|
| 92 |
+
padded[0, : len(ids)] = ids
|
| 93 |
+
|
| 94 |
+
return padded, min(original_len, pad_length)
|
| 95 |
+
|
| 96 |
+
def encode_long(
|
| 97 |
+
self, text: str, window_size: int = 64
|
| 98 |
+
) -> Tuple[List[np.ndarray], List[int], List[int]]:
|
| 99 |
+
"""Tokenize long text into sliding windows for batched inference.
|
| 100 |
+
|
| 101 |
+
Splits full token sequence into windows of window_size.
|
| 102 |
+
Each window is padded to window_size if shorter.
|
| 103 |
+
|
| 104 |
+
Returns:
|
| 105 |
+
windows: list of (1, window_size) int32 arrays
|
| 106 |
+
window_token_ids: list of token ID lists per window
|
| 107 |
+
window_lens: original token lengths per window (before padding)
|
| 108 |
+
"""
|
| 109 |
+
ids = self.tokenize(text)
|
| 110 |
+
if not ids:
|
| 111 |
+
return [], [], []
|
| 112 |
+
|
| 113 |
+
windows = []
|
| 114 |
+
window_token_ids = []
|
| 115 |
+
window_lens = []
|
| 116 |
+
|
| 117 |
+
for start in range(0, len(ids), window_size):
|
| 118 |
+
chunk = ids[start:start + window_size]
|
| 119 |
+
chunk_len = len(chunk)
|
| 120 |
+
|
| 121 |
+
padded = np.zeros((1, window_size), dtype=np.int32)
|
| 122 |
+
padded[0, :chunk_len] = chunk
|
| 123 |
+
|
| 124 |
+
windows.append(padded)
|
| 125 |
+
window_token_ids.append(chunk)
|
| 126 |
+
window_lens.append(chunk_len)
|
| 127 |
+
|
| 128 |
+
return windows, window_token_ids, window_lens
|
tokens.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|