sinonym 0.2.5__tar.gz → 0.2.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sinonym-0.2.5 → sinonym-0.2.7}/PKG-INFO +2 -1
- {sinonym-0.2.5 → sinonym-0.2.7}/pyproject.toml +2 -1
- sinonym-0.2.7/sinonym/timo/__init__.py +0 -0
- sinonym-0.2.7/sinonym/timo/config.yaml +12 -0
- sinonym-0.2.7/sinonym/timo/integration_test.py +70 -0
- sinonym-0.2.7/sinonym/timo/interface.py +244 -0
- sinonym-0.2.7/tests/test_timo_interface.py +87 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/.gitignore +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/LICENSE +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/README.md +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/scripts/README.md +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/__init__.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/chinese_names_data.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/coretypes/__init__.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/coretypes/config.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/coretypes/results.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/data/README.md +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/data/acl_2025_authors.txt +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/data/chinese_japanese_classifier.joblib +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/data/chinese_japanese_classifier.skops +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/data/familyname_orcid.csv +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/data/givenname_orcid.csv +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/data/model_features.json +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/detector.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/ml_model_components.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/patterns/__init__.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/patterns/compiled_patterns.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/patterns/regex_builders.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/resources.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/services/__init__.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/services/batch_analysis.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/services/cache.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/services/ethnicity.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/services/formatting.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/services/initialization.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/services/normalization.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/services/parsing.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/services/process_pool.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/text_processing/__init__.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/text_processing/compound_detector.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/text_processing/text_normalizer.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/text_processing/text_preprocessor.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/utils/__init__.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/utils/string_manipulation.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/sinonym/utils/thread_cache.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/tests/_fail_log.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/tests/conftest.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_acl.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_all_chinese_inputs.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_basic_chinese_names.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_batch.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_compound_names.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_compound_surname_formats.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_middle_names.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_misc.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_mixed_production_cases.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_mixed_scripts.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_ml_japanese_detection.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_multiprocess_pool.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_name_formatting.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_non_chinese_rejection.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_performance.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_regional_variants.py +0 -0
- {sinonym-0.2.5 → sinonym-0.2.7}/tests/test_regression_proposals.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sinonym
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.7
|
|
4
4
|
Summary: Chinese Name Detection and Normalization Module
|
|
5
5
|
Project-URL: Homepage, https://github.com/allenai/sinonym
|
|
6
6
|
Project-URL: Repository, https://github.com/allenai/sinonym
|
|
@@ -21,6 +21,7 @@ Classifier: Topic :: Text Processing :: Linguistic
|
|
|
21
21
|
Requires-Python: >=3.10
|
|
22
22
|
Requires-Dist: joblib>=1.3
|
|
23
23
|
Requires-Dist: numpy>=1.24
|
|
24
|
+
Requires-Dist: pydantic<2,>=1.10
|
|
24
25
|
Requires-Dist: pypinyin>=0.44.0
|
|
25
26
|
Requires-Dist: requests>=2.31
|
|
26
27
|
Requires-Dist: scikit-learn<2,>=1.2
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sinonym"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.7"
|
|
8
8
|
description = "Chinese Name Detection and Normalization Module"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -27,6 +27,7 @@ keywords = ["chinese", "names", "nlp", "romanization", "pinyin"]
|
|
|
27
27
|
dependencies = [
|
|
28
28
|
"joblib>=1.3",
|
|
29
29
|
"numpy>=1.24",
|
|
30
|
+
"pydantic>=1.10,<2",
|
|
30
31
|
"pypinyin>=0.44.0",
|
|
31
32
|
"requests>=2.31",
|
|
32
33
|
"scikit-learn>=1.2,<2",
|
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
config_version: 0.0.1
|
|
2
|
+
model_variants:
|
|
3
|
+
sinonym_v1:
|
|
4
|
+
instance: sinonym.timo.interface.Instance
|
|
5
|
+
prediction: sinonym.timo.interface.Prediction
|
|
6
|
+
predictor: sinonym.timo.interface.Predictor
|
|
7
|
+
predictor_config: sinonym.timo.interface.PredictorConfig
|
|
8
|
+
artifacts_s3_path: null
|
|
9
|
+
python_version: "3.10"
|
|
10
|
+
cuda: False
|
|
11
|
+
integration_test: sinonym.timo.integration_test.TestIntegration
|
|
12
|
+
docker_run_commands: []
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from sinonym.timo.interface import Instance, Prediction, Predictor, PredictorConfig
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestIntegration(unittest.TestCase):
|
|
7
|
+
@classmethod
|
|
8
|
+
def setUpClass(cls):
|
|
9
|
+
cls.predictor = Predictor(config=PredictorConfig(), artifacts_dir=".")
|
|
10
|
+
|
|
11
|
+
def test_chinese_name(self):
|
|
12
|
+
results = self.predictor.predict_batch([Instance(name="Li Wei")])
|
|
13
|
+
self.assertEqual(len(results), 1)
|
|
14
|
+
self.assertIsInstance(results[0], Prediction)
|
|
15
|
+
self.assertTrue(results[0].success)
|
|
16
|
+
self.assertIsNotNone(results[0].given_name)
|
|
17
|
+
self.assertIsNotNone(results[0].surname)
|
|
18
|
+
|
|
19
|
+
def test_non_chinese_name(self):
|
|
20
|
+
results = self.predictor.predict_batch([Instance(name="John Smith")])
|
|
21
|
+
self.assertEqual(len(results), 1)
|
|
22
|
+
self.assertFalse(results[0].success)
|
|
23
|
+
self.assertIsNotNone(results[0].error_message)
|
|
24
|
+
|
|
25
|
+
def test_batch_superset_output(self):
|
|
26
|
+
instances = [Instance(name="Li Wei"), Instance(name="Wang Weiming")]
|
|
27
|
+
results = self.predictor.predict_batch(instances)
|
|
28
|
+
self.assertEqual(len(results), 2)
|
|
29
|
+
for r in results:
|
|
30
|
+
self.assertIsInstance(r, Prediction)
|
|
31
|
+
self.assertTrue(r.success)
|
|
32
|
+
# superset: every Prediction carries confidence + format_pattern
|
|
33
|
+
self.assertIsNotNone(r.confidence)
|
|
34
|
+
self.assertIsNotNone(r.format_pattern)
|
|
35
|
+
# shared batch pattern replicated onto each row
|
|
36
|
+
self.assertEqual(
|
|
37
|
+
results[0].format_pattern.dominant_format,
|
|
38
|
+
results[1].format_pattern.dominant_format,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def test_predict_batch_empty(self):
|
|
42
|
+
self.assertEqual(self.predictor.predict_batch([]), [])
|
|
43
|
+
|
|
44
|
+
def test_score_name_batch(self):
|
|
45
|
+
names = ["Li Wei", "Wang Weiming"]
|
|
46
|
+
summary = self.predictor.score_name_batch(names)
|
|
47
|
+
self.assertEqual(summary.names, names)
|
|
48
|
+
self.assertEqual(len(summary.results), 2)
|
|
49
|
+
self.assertEqual(len(summary.confidences), 2)
|
|
50
|
+
self.assertIsNotNone(summary.format_pattern.dominant_format)
|
|
51
|
+
|
|
52
|
+
def test_score_name_batch_tuned_threshold(self):
|
|
53
|
+
names = ["Li Wei", "Wang Weiming"]
|
|
54
|
+
summary = self.predictor.score_name_batch(names, format_threshold=0.9)
|
|
55
|
+
self.assertEqual(len(summary.results), 2)
|
|
56
|
+
self.assertEqual(
|
|
57
|
+
summary.format_pattern.threshold_met,
|
|
58
|
+
summary.format_pattern.confidence >= 0.9,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def test_detect_batch_format(self):
|
|
62
|
+
pattern = self.predictor.detect_batch_format(["Zhang Wei", "Li Ming", "Wang Xiaoli"])
|
|
63
|
+
self.assertIn(
|
|
64
|
+
pattern.dominant_format, {"surname_first", "given_first", "mixed"}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def test_analyze_name_batch_full(self):
|
|
68
|
+
result = self.predictor.analyze_name_batch(["Li Wei", "Wang Weiming"])
|
|
69
|
+
self.assertEqual(len(result.results), 2)
|
|
70
|
+
self.assertEqual(len(result.individual_analyses), 2)
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, BaseSettings, Field
|
|
4
|
+
|
|
5
|
+
from sinonym.detector import ChineseNameDetector
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Instance(BaseModel):
|
|
9
|
+
name: str = Field(description="Name string to detect/normalize as Chinese")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FormatPattern(BaseModel):
|
|
13
|
+
"""Batch-level order detection (surname-first vs given-first)."""
|
|
14
|
+
|
|
15
|
+
dominant_format: str = Field(description="surname_first | given_first | mixed")
|
|
16
|
+
confidence: float = Field(description="dominant_count / total_count")
|
|
17
|
+
surname_first_count: int
|
|
18
|
+
given_first_count: int
|
|
19
|
+
total_count: int
|
|
20
|
+
threshold_met: bool = Field(description="confidence >= format_threshold")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Prediction(BaseModel):
|
|
24
|
+
success: bool = Field(description="Whether the name was recognized as Chinese")
|
|
25
|
+
error_message: Optional[str] = Field(default=None, description="Reason for failure")
|
|
26
|
+
given_name: Optional[str] = Field(default=None)
|
|
27
|
+
surname: Optional[str] = Field(default=None)
|
|
28
|
+
middle_name: Optional[str] = Field(default=None)
|
|
29
|
+
confidence: Optional[float] = Field(
|
|
30
|
+
default=None, description="per-name confidence (softmax over candidate scores)"
|
|
31
|
+
)
|
|
32
|
+
format_pattern: Optional[FormatPattern] = Field(
|
|
33
|
+
default=None, description="shared batch order pattern (same on every row)"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Candidate(BaseModel):
|
|
38
|
+
surname_tokens: List[str]
|
|
39
|
+
given_tokens: List[str]
|
|
40
|
+
score: float
|
|
41
|
+
format: str = Field(description="surname_first | given_first | mixed")
|
|
42
|
+
original_compound_format: Optional[str] = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class IndividualAnalysis(BaseModel):
|
|
46
|
+
"""Per-name analysis, pre batch-override."""
|
|
47
|
+
|
|
48
|
+
raw_name: str
|
|
49
|
+
candidates: List[Candidate]
|
|
50
|
+
best_candidate: Optional[Candidate] = None
|
|
51
|
+
confidence: float = Field(description="softmax over candidate scores for best candidate")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class BatchPrediction(BaseModel):
|
|
55
|
+
"""Full result of analyze_name_batch."""
|
|
56
|
+
|
|
57
|
+
names: List[str]
|
|
58
|
+
results: List[Prediction]
|
|
59
|
+
format_pattern: FormatPattern
|
|
60
|
+
individual_analyses: List[IndividualAnalysis]
|
|
61
|
+
improvements: List[int] = Field(description="indices of names changed by batch context")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class BatchSummary(BaseModel):
|
|
65
|
+
"""Trimmed analyze_name_batch result: drops candidates, keeps per-name confidence only."""
|
|
66
|
+
|
|
67
|
+
names: List[str]
|
|
68
|
+
results: List[Prediction]
|
|
69
|
+
format_pattern: FormatPattern
|
|
70
|
+
confidences: List[float] = Field(
|
|
71
|
+
description="per-name confidence from individual_analyses, aligned with results"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class PredictorConfig(BaseSettings):
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Predictor:
|
|
80
|
+
_config: PredictorConfig
|
|
81
|
+
_artifacts_dir: str
|
|
82
|
+
|
|
83
|
+
def __init__(self, config: PredictorConfig, artifacts_dir: str):
|
|
84
|
+
self._config = config
|
|
85
|
+
self._artifacts_dir = artifacts_dir
|
|
86
|
+
self._detector = ChineseNameDetector()
|
|
87
|
+
|
|
88
|
+
# ---- converters -------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
def _to_prediction(self, parse_result) -> Prediction:
|
|
91
|
+
return Prediction(
|
|
92
|
+
success=parse_result.success,
|
|
93
|
+
error_message=parse_result.error_message,
|
|
94
|
+
given_name=parse_result.parsed.given_name if parse_result.parsed else None,
|
|
95
|
+
surname=parse_result.parsed.surname if parse_result.parsed else None,
|
|
96
|
+
middle_name=(
|
|
97
|
+
parse_result.parsed.middle_name
|
|
98
|
+
if parse_result.parsed and parse_result.parsed.middle_name
|
|
99
|
+
else None
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def _to_format_pattern(self, pattern) -> FormatPattern:
|
|
104
|
+
return FormatPattern(
|
|
105
|
+
dominant_format=pattern.dominant_format.value,
|
|
106
|
+
confidence=pattern.confidence,
|
|
107
|
+
surname_first_count=pattern.surname_first_count,
|
|
108
|
+
given_first_count=pattern.given_first_count,
|
|
109
|
+
total_count=pattern.total_count,
|
|
110
|
+
threshold_met=pattern.threshold_met,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def _to_candidate(self, candidate) -> Candidate:
|
|
114
|
+
return Candidate(
|
|
115
|
+
surname_tokens=list(candidate.surname_tokens),
|
|
116
|
+
given_tokens=list(candidate.given_tokens),
|
|
117
|
+
score=candidate.score,
|
|
118
|
+
format=candidate.format.value,
|
|
119
|
+
original_compound_format=candidate.original_compound_format,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def _to_individual_analysis(self, analysis) -> IndividualAnalysis:
|
|
123
|
+
return IndividualAnalysis(
|
|
124
|
+
raw_name=analysis.raw_name,
|
|
125
|
+
candidates=[self._to_candidate(c) for c in analysis.candidates],
|
|
126
|
+
best_candidate=(
|
|
127
|
+
self._to_candidate(analysis.best_candidate)
|
|
128
|
+
if analysis.best_candidate is not None
|
|
129
|
+
else None
|
|
130
|
+
),
|
|
131
|
+
confidence=analysis.confidence,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def _to_batch_prediction(self, batch_result) -> BatchPrediction:
|
|
135
|
+
return BatchPrediction(
|
|
136
|
+
names=list(batch_result.names),
|
|
137
|
+
results=[self._to_prediction(r) for r in batch_result.results],
|
|
138
|
+
format_pattern=self._to_format_pattern(batch_result.format_pattern),
|
|
139
|
+
individual_analyses=[
|
|
140
|
+
self._to_individual_analysis(a) for a in batch_result.individual_analyses
|
|
141
|
+
],
|
|
142
|
+
improvements=list(batch_result.improvements),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def _to_batch_summary(self, batch_result) -> BatchSummary:
|
|
146
|
+
return BatchSummary(
|
|
147
|
+
names=list(batch_result.names),
|
|
148
|
+
results=[self._to_prediction(r) for r in batch_result.results],
|
|
149
|
+
format_pattern=self._to_format_pattern(batch_result.format_pattern),
|
|
150
|
+
confidences=[a.confidence for a in batch_result.individual_analyses],
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# ---- timo entrypoint --------------------------------------------------
|
|
154
|
+
|
|
155
|
+
def predict_batch(self, instances: List[Instance]) -> List[Prediction]:
|
|
156
|
+
"""timo HTTP entrypoint. Analyzes the whole batch jointly.
|
|
157
|
+
|
|
158
|
+
Names are processed together (cross-batch order detection), returning one
|
|
159
|
+
Prediction per name (index-aligned) with surname/given_name/middle_name,
|
|
160
|
+
per-name `confidence`, and the shared `format_pattern` (same on every row).
|
|
161
|
+
"""
|
|
162
|
+
if not instances:
|
|
163
|
+
return []
|
|
164
|
+
|
|
165
|
+
names = [i.name for i in instances]
|
|
166
|
+
batch_result = self._detector.analyze_name_batch(names)
|
|
167
|
+
pattern = self._to_format_pattern(batch_result.format_pattern)
|
|
168
|
+
|
|
169
|
+
predictions = []
|
|
170
|
+
for parse_result, analysis in zip(
|
|
171
|
+
batch_result.results, batch_result.individual_analyses
|
|
172
|
+
):
|
|
173
|
+
prediction = self._to_prediction(parse_result)
|
|
174
|
+
prediction.confidence = analysis.confidence
|
|
175
|
+
prediction.format_pattern = pattern
|
|
176
|
+
predictions.append(prediction)
|
|
177
|
+
return predictions
|
|
178
|
+
|
|
179
|
+
# ---- exposed detector functions --------------------------------------
|
|
180
|
+
|
|
181
|
+
@staticmethod
|
|
182
|
+
def _batch_kwargs(format_threshold, minimum_batch_size=...) -> dict:
|
|
183
|
+
"""Forward only caller-set tuning params; let sinonym own the defaults."""
|
|
184
|
+
kw = {}
|
|
185
|
+
if format_threshold is not None:
|
|
186
|
+
kw["format_threshold"] = format_threshold
|
|
187
|
+
if minimum_batch_size is not ... and minimum_batch_size is not None:
|
|
188
|
+
kw["minimum_batch_size"] = minimum_batch_size
|
|
189
|
+
return kw
|
|
190
|
+
|
|
191
|
+
def analyze_name_batch(
|
|
192
|
+
self,
|
|
193
|
+
names: List[str],
|
|
194
|
+
format_threshold: Optional[float] = None,
|
|
195
|
+
minimum_batch_size: Optional[int] = None,
|
|
196
|
+
) -> BatchPrediction:
|
|
197
|
+
batch_result = self._detector.analyze_name_batch(
|
|
198
|
+
names, **self._batch_kwargs(format_threshold, minimum_batch_size)
|
|
199
|
+
)
|
|
200
|
+
return self._to_batch_prediction(batch_result)
|
|
201
|
+
|
|
202
|
+
def process_name_batch(
|
|
203
|
+
self,
|
|
204
|
+
names: List[str],
|
|
205
|
+
format_threshold: Optional[float] = None,
|
|
206
|
+
minimum_batch_size: Optional[int] = None,
|
|
207
|
+
) -> List[Prediction]:
|
|
208
|
+
results = self._detector.process_name_batch(
|
|
209
|
+
names, **self._batch_kwargs(format_threshold, minimum_batch_size)
|
|
210
|
+
)
|
|
211
|
+
return [self._to_prediction(r) for r in results]
|
|
212
|
+
|
|
213
|
+
def detect_batch_format(
|
|
214
|
+
self,
|
|
215
|
+
names: List[str],
|
|
216
|
+
format_threshold: Optional[float] = None,
|
|
217
|
+
) -> FormatPattern:
|
|
218
|
+
pattern = self._detector.detect_batch_format(
|
|
219
|
+
names, **self._batch_kwargs(format_threshold)
|
|
220
|
+
)
|
|
221
|
+
return self._to_format_pattern(pattern)
|
|
222
|
+
|
|
223
|
+
def process_name_batch_multiprocess(
|
|
224
|
+
self,
|
|
225
|
+
names: List[str],
|
|
226
|
+
max_workers: Optional[int] = None,
|
|
227
|
+
chunk_size: int = 64,
|
|
228
|
+
) -> List[Prediction]:
|
|
229
|
+
results = self._detector.process_name_batch_multiprocess(
|
|
230
|
+
names, max_workers=max_workers, chunk_size=chunk_size
|
|
231
|
+
)
|
|
232
|
+
return [self._to_prediction(r) for r in results]
|
|
233
|
+
|
|
234
|
+
def score_name_batch(
|
|
235
|
+
self,
|
|
236
|
+
names: List[str],
|
|
237
|
+
format_threshold: Optional[float] = None,
|
|
238
|
+
minimum_batch_size: Optional[int] = None,
|
|
239
|
+
) -> BatchSummary:
|
|
240
|
+
"""analyze_name_batch trimmed to names, results, format_pattern, per-name confidence."""
|
|
241
|
+
batch_result = self._detector.analyze_name_batch(
|
|
242
|
+
names, **self._batch_kwargs(format_threshold, minimum_batch_size)
|
|
243
|
+
)
|
|
244
|
+
return self._to_batch_summary(batch_result)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from sinonym.timo.interface import Instance, Prediction, Predictor, PredictorConfig
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestTimoInterface(unittest.TestCase):
|
|
7
|
+
@classmethod
|
|
8
|
+
def setUpClass(cls):
|
|
9
|
+
cls.predictor = Predictor(config=PredictorConfig(), artifacts_dir=".")
|
|
10
|
+
|
|
11
|
+
def test_single_chinese_name(self):
|
|
12
|
+
results = self.predictor.predict_batch([Instance(name="Li Wei")])
|
|
13
|
+
self.assertEqual(len(results), 1)
|
|
14
|
+
r = results[0]
|
|
15
|
+
self.assertTrue(r.success)
|
|
16
|
+
self.assertIsNotNone(r.surname)
|
|
17
|
+
self.assertIsNotNone(r.given_name)
|
|
18
|
+
self.assertIsNotNone(r.confidence)
|
|
19
|
+
self.assertIsNone(r.error_message)
|
|
20
|
+
|
|
21
|
+
def test_chinese_name_characters(self):
|
|
22
|
+
results = self.predictor.predict_batch([Instance(name="巩俐")])
|
|
23
|
+
self.assertTrue(results[0].success)
|
|
24
|
+
self.assertIsNotNone(results[0].surname)
|
|
25
|
+
|
|
26
|
+
def test_chinese_name_comma_format(self):
|
|
27
|
+
results = self.predictor.predict_batch([Instance(name="Zhang, Ming")])
|
|
28
|
+
r = results[0]
|
|
29
|
+
self.assertTrue(r.success)
|
|
30
|
+
self.assertIsNotNone(r.surname)
|
|
31
|
+
self.assertIsNotNone(r.given_name)
|
|
32
|
+
|
|
33
|
+
def test_non_chinese_name(self):
|
|
34
|
+
results = self.predictor.predict_batch([Instance(name="John Smith")])
|
|
35
|
+
r = results[0]
|
|
36
|
+
self.assertFalse(r.success)
|
|
37
|
+
self.assertIsNotNone(r.error_message)
|
|
38
|
+
self.assertIsNone(r.given_name)
|
|
39
|
+
self.assertIsNone(r.surname)
|
|
40
|
+
|
|
41
|
+
def test_empty_string(self):
|
|
42
|
+
results = self.predictor.predict_batch([Instance(name="")])
|
|
43
|
+
self.assertFalse(results[0].success)
|
|
44
|
+
|
|
45
|
+
def test_whitespace_only(self):
|
|
46
|
+
results = self.predictor.predict_batch([Instance(name=" ")])
|
|
47
|
+
self.assertFalse(results[0].success)
|
|
48
|
+
|
|
49
|
+
def test_batch_with_context(self):
|
|
50
|
+
instances = [
|
|
51
|
+
Instance(name="Li Wei"),
|
|
52
|
+
Instance(name="Wang Weiming"),
|
|
53
|
+
Instance(name="John Smith"),
|
|
54
|
+
]
|
|
55
|
+
results = self.predictor.predict_batch(instances)
|
|
56
|
+
self.assertEqual(len(results), 3)
|
|
57
|
+
self.assertTrue(results[0].success)
|
|
58
|
+
self.assertTrue(results[1].success)
|
|
59
|
+
self.assertFalse(results[2].success)
|
|
60
|
+
|
|
61
|
+
def test_prediction_is_pydantic_model(self):
|
|
62
|
+
results = self.predictor.predict_batch([Instance(name="Li Wei")])
|
|
63
|
+
r = results[0]
|
|
64
|
+
self.assertIsInstance(r, Prediction)
|
|
65
|
+
as_dict = r.dict()
|
|
66
|
+
self.assertIn("success", as_dict)
|
|
67
|
+
self.assertIn("error_message", as_dict)
|
|
68
|
+
self.assertIn("given_name", as_dict)
|
|
69
|
+
self.assertIn("surname", as_dict)
|
|
70
|
+
self.assertIn("middle_name", as_dict)
|
|
71
|
+
self.assertIn("confidence", as_dict)
|
|
72
|
+
self.assertIn("format_pattern", as_dict)
|
|
73
|
+
|
|
74
|
+
def test_prediction_json_serialization(self):
|
|
75
|
+
results = self.predictor.predict_batch([Instance(name="Li Wei")])
|
|
76
|
+
json_str = results[0].json()
|
|
77
|
+
self.assertIn('"success": true', json_str)
|
|
78
|
+
self.assertIn('"confidence":', json_str)
|
|
79
|
+
|
|
80
|
+
def test_compound_surname(self):
|
|
81
|
+
results = self.predictor.predict_batch([Instance(name="Ouyang Ming")])
|
|
82
|
+
if results[0].success:
|
|
83
|
+
self.assertIsNotNone(results[0].surname)
|
|
84
|
+
|
|
85
|
+
def test_empty_batch(self):
|
|
86
|
+
results = self.predictor.predict_batch([])
|
|
87
|
+
self.assertEqual(len(results), 0)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|