inoryQwQ commited on
Commit
76aca0a
·
verified ·
1 Parent(s): b08e8a6

Upload folder using huggingface_hub

Browse files
.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