twfilter 0.1.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +37 -0
- data/LICENSE +19 -0
- data/NOTICES.md +93 -0
- data/README.md +364 -0
- data/lib/twfilter/block.rb +36 -0
- data/lib/twfilter/checks/erhua.rb +40 -0
- data/lib/twfilter/checks/lexicon.rb +83 -0
- data/lib/twfilter/checks/script.rb +71 -0
- data/lib/twfilter/checks/shape.rb +40 -0
- data/lib/twfilter/checks/wenyan.rb +32 -0
- data/lib/twfilter/data/MANIFEST.json +85 -0
- data/lib/twfilter/data/cantonese.txt +17 -0
- data/lib/twfilter/data/converted_orthography.txt +14 -0
- data/lib/twfilter/data/erhua_headed.txt +14 -0
- data/lib/twfilter/data/erhua_tailed.txt +36 -0
- data/lib/twfilter/data/foreign_topics.txt +80 -0
- data/lib/twfilter/data/mainland_exceptions.tsv +6 -0
- data/lib/twfilter/data/mainland_hard.tsv +149 -0
- data/lib/twfilter/data/mainland_soft.tsv +38 -0
- data/lib/twfilter/data/moe_common.txt +4808 -0
- data/lib/twfilter/data/moe_exception.txt +12 -0
- data/lib/twfilter/data/moe_rare.txt +18356 -0
- data/lib/twfilter/data/moe_secondary.txt +6343 -0
- data/lib/twfilter/data/regional_hard.tsv +19 -0
- data/lib/twfilter/data/simplified_only.txt +3785 -0
- data/lib/twfilter/data/taiwan_grammar.txt +20 -0
- data/lib/twfilter/data/taiwan_lexicon.txt +51 -0
- data/lib/twfilter/data/taiwan_markers.txt +66 -0
- data/lib/twfilter/data/taiwan_particles.txt +7 -0
- data/lib/twfilter/data/variants_used_in_taiwan.txt +16 -0
- data/lib/twfilter/data/wenyan.txt +4 -0
- data/lib/twfilter/errors.rb +12 -0
- data/lib/twfilter/evidence.rb +44 -0
- data/lib/twfilter/finding.rb +42 -0
- data/lib/twfilter/han.rb +30 -0
- data/lib/twfilter/policy.rb +68 -0
- data/lib/twfilter/punctuation.rb +106 -0
- data/lib/twfilter/sentences.rb +32 -0
- data/lib/twfilter/subject.rb +28 -0
- data/lib/twfilter/tables.rb +140 -0
- data/lib/twfilter/version.rb +5 -0
- data/lib/twfilter.rb +57 -0
- data/licenses/APACHE-2.0.txt +202 -0
- data/sig/twfilter.rbs +215 -0
- metadata +137 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9fa81150f2ea0cdd0793e84dc5bca8b568fe7ab699693b90a6023a3933e00e40
|
|
4
|
+
data.tar.gz: 1264fea5c38f14348f7b2767acaf1583c89b793c341eebfd5864e106f38070a9
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: dd27ae0c7be37abc15e95a0b014d07cb41c8096fd4ad8a98b5574309e3706155f53086360ccea33d20e009399270f23ccd2ab08cfed514ca1f67ef114d89632a
|
|
7
|
+
data.tar.gz: 7aa05c02c19e57255e452def5ba72ea17aa253d5ea4aa485fc51eba965740bb9a10ccd940be1e5e3b791e959d4962f3727e452b5cb6960556fbc5bf2dbc3d2ed
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — 2026-08-01
|
|
4
|
+
|
|
5
|
+
First release.
|
|
6
|
+
|
|
7
|
+
- Provenance analysis over five independent checks, each reporting typed findings with a
|
|
8
|
+
severity rather than a boolean (`TWFilter::Report`, `TWFilter::Finding`)
|
|
9
|
+
- MOE character-chart tiering: 常用 4 808, 次常用 6 343, 罕用 18 356, plus 12 dictionary
|
|
10
|
+
headword characters outside all three charts. Any Han character outside that inventory is
|
|
11
|
+
rejected unconditionally (`TWFilter::Checks::Script`)
|
|
12
|
+
- Simplified-form detection derived from the OpenCC `STCharacters` / `TSCharacters`
|
|
13
|
+
round-trip, minus 16 forms that appear in an MOE chart despite occurring in the
|
|
14
|
+
simplification tables
|
|
15
|
+
- Mainland traditional orthography: 裏着衞爲説麽綫墻眞啓衆産囯麪
|
|
16
|
+
- Mainland lexical markers graded by attestation against a native reference corpus:
|
|
17
|
+
149 rejecting, 38 marking, 6 exception masks (`TWFilter::Checks::Lexicon`)
|
|
18
|
+
- Hong Kong, written-Cantonese and Singapore markers: 17 characters and 19 word forms
|
|
19
|
+
- 80 non-Taiwanese toponyms and institutions, tagged rather than rejected, with an address
|
|
20
|
+
guard for the Taipei streets named after mainland cities
|
|
21
|
+
- Erhua two-window analysis over 兒 as a root morpheme (`TWFilter::Checks::Erhua`)
|
|
22
|
+
- Literary Chinese density over 矣哉汝吾 (`TWFilter::Checks::Wenyan`)
|
|
23
|
+
- Positive Taiwan evidence over lexicon, sentence-final particles, ROC institutions and the
|
|
24
|
+
有 + V perfective outside negation (`TWFilter::Evidence`)
|
|
25
|
+
- Punctuation normalization to 教育部《重訂標點符號手冊》: quotation brackets, the
|
|
26
|
+
U+2027 separator, six-dot ellipsis, box-drawing dash, width folding, invisible removal
|
|
27
|
+
(`TWFilter::Punctuation`)
|
|
28
|
+
- Sentence segmentation over terminator and closer classes, linear in input length
|
|
29
|
+
(`TWFilter::Sentences`)
|
|
30
|
+
- Block-level judgement with a contamination tolerance and an evidence floor
|
|
31
|
+
(`TWFilter::Block`)
|
|
32
|
+
- Three policies — `corpus`, `publishable`, `permissive` — as immutable value objects
|
|
33
|
+
(`TWFilter::Policy`)
|
|
34
|
+
- Reference tables resolvable from an external directory, with a checksum manifest and a
|
|
35
|
+
provenance fingerprint for recording alongside derived measurements (`TWFilter::Tables`)
|
|
36
|
+
- A stated membership rule per table, enforced against the shipped data
|
|
37
|
+
(`spec/twfilter/membership_spec.rb`)
|
data/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Den Patin <hi@dpat.in>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
|
6
|
+
software and associated documentation files (the "Software"), to deal in the Software
|
|
7
|
+
without restriction, including without limitation the rights to use, copy, modify, merge,
|
|
8
|
+
publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
|
9
|
+
to whom the Software is furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all copies or
|
|
12
|
+
substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
15
|
+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
|
16
|
+
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
|
17
|
+
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
18
|
+
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
19
|
+
DEALINGS IN THE SOFTWARE.
|
data/NOTICES.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Notices
|
|
2
|
+
|
|
3
|
+
The code in this gem is MIT. The reference tables under `lib/twfilter/data/` are not all
|
|
4
|
+
mine. This file records the provenance and terms of each. **Every table may be used in
|
|
5
|
+
commercial products**, subject to the attribution stated below.
|
|
6
|
+
|
|
7
|
+
## Table provenance
|
|
8
|
+
|
|
9
|
+
| Table | Source | Terms |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| `moe_common.txt`, `moe_secondary.txt`, `moe_rare.txt` | 全字庫 CNS 11643, field `CNS_source`, entries marked 常用國字標準字體表 / 次常用國字標準字體表 / 罕用字體表 | OGDL v1.0 |
|
|
12
|
+
| `moe_exception.txt` | Characters occurring as headwords in the MOE dictionaries but in none of the three charts | Factual inventory; see below |
|
|
13
|
+
| `simplified_only.txt` | Computed from OpenCC `STCharacters` ∖ `TSCharacters` | Apache-2.0 |
|
|
14
|
+
| `mainland_hard.tsv`, `mainland_soft.tsv` | Candidate pairs from OpenCC `TWPhrases` and from hand curation; retention and banding computed by me | Apache-2.0 |
|
|
15
|
+
| all other tables | Authored by me | MIT |
|
|
16
|
+
|
|
17
|
+
## OGDL v1.0 — 政府資料開放授權條款 第1版
|
|
18
|
+
|
|
19
|
+
Applies to the three character charts, obtained from 全字庫 (數位發展部), which offers its
|
|
20
|
+
data under OGDL v1.0 or SIL OFL 1.1 at the user's option. I take OGDL v1.0.
|
|
21
|
+
|
|
22
|
+
Grants: unrestricted use, reproduction, adaptation, distribution and public transmission,
|
|
23
|
+
including for commercial purposes and in derivative products, worldwide and without time
|
|
24
|
+
limit.
|
|
25
|
+
|
|
26
|
+
Requires: attribution (註明出處). **Failure to attribute voids the grant ab initio.**
|
|
27
|
+
|
|
28
|
+
Attribution string:
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
數位發展部,CNS11643中文標準交換碼全字庫網站
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Apache-2.0 — OpenCC
|
|
35
|
+
|
|
36
|
+
Applies to `simplified_only.txt` and to the candidate pairs behind `mainland_hard.tsv` and
|
|
37
|
+
`mainland_soft.tsv`. Full text in `licenses/APACHE-2.0.txt` (§4(a)). OpenCC ships no NOTICE
|
|
38
|
+
file, so §4(d) does not apply.
|
|
39
|
+
|
|
40
|
+
Per §4(b), the derived files are **modified**:
|
|
41
|
+
|
|
42
|
+
- `simplified_only.txt` is a set difference over two OpenCC conversion tables, further
|
|
43
|
+
reduced by 16 forms standard in Taiwan. It is not an OpenCC table and does not convert.
|
|
44
|
+
- `mainland_hard.tsv` and `mainland_soft.tsv` retain 187 of 780 candidate pairs. Retention,
|
|
45
|
+
banding and the attestation counts are mine. The excluded 593 are excluded because
|
|
46
|
+
`TWPhrases` is an orthographic mapping, not a provenance signal.
|
|
47
|
+
|
|
48
|
+
Attribution string:
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
OpenCC — https://github.com/BYVoid/OpenCC, Apache License 2.0
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## MOE dictionaries — CC BY-ND 3.0 TW
|
|
55
|
+
|
|
56
|
+
`moe_exception.txt` lists 12 characters found as dictionary headwords. A character
|
|
57
|
+
inventory states which characters occur; it reproduces no definition, no example and no
|
|
58
|
+
expression, and is therefore neither a copy nor an adaptation of the dictionaries. Taiwan
|
|
59
|
+
著作權法 art. 9(1)(3) excludes 通用之符號 and 表格 from protection for want of creativity.
|
|
60
|
+
|
|
61
|
+
Attribution is given regardless:
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
中華民國教育部《重編國語辭典修訂本》《國語辭典簡編本》
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The ND term restricts distribution of adaptations. No dictionary text is shipped here, so
|
|
68
|
+
the term is not engaged. Consumers who ship dictionary *content* must satisfy it
|
|
69
|
+
themselves.
|
|
70
|
+
|
|
71
|
+
## My own tables
|
|
72
|
+
|
|
73
|
+
`variants_used_in_taiwan.txt`, `converted_orthography.txt`, `cantonese.txt`, `wenyan.txt`,
|
|
74
|
+
`mainland_exceptions.tsv`, `regional_hard.tsv`, `foreign_topics.txt`, `taiwan_markers.txt`,
|
|
75
|
+
`taiwan_lexicon.txt`, `taiwan_particles.txt`, `taiwan_grammar.txt`, `erhua_headed.txt`,
|
|
76
|
+
`erhua_tailed.txt` — MIT, same as the code.
|
|
77
|
+
|
|
78
|
+
These are word lists. Individual words are not copyrightable; the selection and arrangement
|
|
79
|
+
are mine and are released under MIT.
|
|
80
|
+
|
|
81
|
+
## What a consumer must do
|
|
82
|
+
|
|
83
|
+
Shipping this gem in a commercial product requires reproducing two attribution strings —
|
|
84
|
+
全字庫 and OpenCC — somewhere a user can reach, and including `licenses/APACHE-2.0.txt`.
|
|
85
|
+
`NOTICES.md` satisfies both if shipped as is.
|
|
86
|
+
|
|
87
|
+
Nothing here restricts commercial use, field of endeavour, or the license of the work you
|
|
88
|
+
build. No table carries a NonCommercial, ShareAlike or copyleft term.
|
|
89
|
+
|
|
90
|
+
## Downstream output
|
|
91
|
+
|
|
92
|
+
Verdicts, findings and counts produced by this gem are yours. They are measurements over
|
|
93
|
+
your input, not derivatives of these tables.
|
data/README.md
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
# twfilter
|
|
2
|
+
|
|
3
|
+
Provenance analysis for Chinese text: is this span Taiwan Mandarin (臺灣華語), and if not,
|
|
4
|
+
on what evidence. Pure Ruby, no runtime dependencies, no native extensions, no network,
|
|
5
|
+
deterministic output.
|
|
6
|
+
|
|
7
|
+
Traditional Chinese is written in Taiwan, Hong Kong and Macau, and by mainland publishers
|
|
8
|
+
converting from simplified. Script alone does not identify origin. Every string below is
|
|
9
|
+
written in traditional characters, and only three of them are Taiwan Mandarin.
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
TWFilter.keep?("這個政策有沒有經過完整評估?") # => true
|
|
13
|
+
TWFilter.keep?("请把这个信息转发给同事") # => false (simplified script)
|
|
14
|
+
TWFilter.keep?("請把這個信息轉發給同事") # => false (lexicon: 信息, Taiwan writes 資訊)
|
|
15
|
+
TWFilter.keep?("他站在門裏面等了很久") # => false (orthography: 裏, Taiwan writes 裡)
|
|
16
|
+
TWFilter.keep?("佢哋唔知道發生咗咩事") # => false (Cantonese particles)
|
|
17
|
+
TWFilter.keep?("這事兒辦得挺漂亮的") # => false (erhua suffix on 事)
|
|
18
|
+
TWFilter.keep?("他的女兒今年考上大學") # => true (兒 here is a root morpheme)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Depth matters as much as breadth. Conversion tables are not provenance signals, and
|
|
22
|
+
subject matter is not provenance either:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
# 程序 is ordinary Taiwanese legal vocabulary, not a mainland form
|
|
26
|
+
TWFilter.keep?("行政程序法所定之程序應由主管機關進行") # => true
|
|
27
|
+
|
|
28
|
+
# 了嗎 is attested in every native Taiwanese source; it is not a marker
|
|
29
|
+
TWFilter.keep?("你吃飯了嗎?我們等一下再出發。") # => true
|
|
30
|
+
|
|
31
|
+
# Ambiguous vocabulary is flagged for review, not discarded
|
|
32
|
+
TWFilter.examine("這個視頻的畫質很不錯").marks.map(&:code) # => [:mainland_soft]
|
|
33
|
+
|
|
34
|
+
# Taiwanese reporting about China is Taiwan Mandarin; the topic is tagged, not rejected
|
|
35
|
+
TWFilter.examine("行政院昨天回應了北京的最新聲明").marks.map(&:code) # => [:foreign_topic]
|
|
36
|
+
|
|
37
|
+
# 南京 is also a Taipei street; an address suffix suppresses the topic tag
|
|
38
|
+
TWFilter.examine("臺北市南京東路五段的捷運站出口").findings # => []
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Provenance is decided on orthography, character inventory, lexical choice, morphology and
|
|
42
|
+
a positive evidence signal, and every criterion that fired is reported.
|
|
43
|
+
|
|
44
|
+
## What it does and does not do
|
|
45
|
+
|
|
46
|
+
**Does** — decide provenance of a span of text; normalize punctuation to the Taiwan
|
|
47
|
+
standard; segment into sentences; report a character-difficulty tier; report every reason
|
|
48
|
+
for every decision.
|
|
49
|
+
|
|
50
|
+
**Does not** — segment words, tag parts of speech, detect language (assume Han script
|
|
51
|
+
input), translate, convert between simplified and traditional, or judge grammaticality.
|
|
52
|
+
A span rejected here is not ungrammatical; it is *not Taiwanese*.
|
|
53
|
+
|
|
54
|
+
## Applications
|
|
55
|
+
|
|
56
|
+
1. **Learner-facing corpora.** Extracted from [TaiwanCards](https://taiwancards.com), where
|
|
57
|
+
it gates every sentence, gloss and collocation before it can reach a card. The
|
|
58
|
+
`publishable` policy encodes the boundary of the pedagogical inventory: MOE 常用 4 808
|
|
59
|
+
only, no ambiguous vocabulary, MOE punctuation only.
|
|
60
|
+
2. **Web corpus construction.** Filtering crawl output where the language identifier
|
|
61
|
+
reports `zho_Hant` but the provenance is unknown. Traditional-script crawls conflate
|
|
62
|
+
four written standards; this separates them at 10⁴ sentences per second per core.
|
|
63
|
+
3. **Training-set curation.** Building or auditing a monolingual set for a model intended
|
|
64
|
+
to produce Taiwan Mandarin rather than converted mainland text. `Report#findings` is
|
|
65
|
+
machine-readable, so exclusions stay attributable.
|
|
66
|
+
4. **Editorial and localization QA.** Detecting mainland vocabulary, orthography and
|
|
67
|
+
punctuation in copy that is supposed to be Taiwanese — the `mainland_soft` band is
|
|
68
|
+
designed for review queues rather than automatic rejection.
|
|
69
|
+
|
|
70
|
+
## Decision procedure
|
|
71
|
+
|
|
72
|
+
```mermaid
|
|
73
|
+
flowchart TD
|
|
74
|
+
T["input span"] --> N["Punctuation.normalize<br/>教育部《重訂標點符號手冊》"]
|
|
75
|
+
N --> SH
|
|
76
|
+
|
|
77
|
+
subgraph SH["Checks::Shape"]
|
|
78
|
+
A1["han count ∈ policy.han_range"]
|
|
79
|
+
A2["han ratio ≥ policy.min_han_ratio"]
|
|
80
|
+
A3["inventory ⊆ Han ∪ Latin ∪ Bopomofo ∪ punctuation"]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
SH --> SC
|
|
84
|
+
subgraph SC["Checks::Script — character inventory"]
|
|
85
|
+
B1["simplified-only set<br/>OpenCC STCharacters ∖ TSCharacters ∖ Taiwan variants"]
|
|
86
|
+
B2["converted orthography 裏着衞爲説麽綫墻眞啓衆産囯麪"]
|
|
87
|
+
B3["MOE tier ≤ policy.max_tier"]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
SC --> LX
|
|
91
|
+
subgraph LX["Checks::Lexicon, Erhua, Wenyan"]
|
|
92
|
+
C1["mainland hard ×149 → reject"]
|
|
93
|
+
C2["mainland soft ×38 → mark"]
|
|
94
|
+
C3["Cantonese characters ×17, HK/SEA lexis ×19 → reject"]
|
|
95
|
+
C4["foreign topics ×80 → mark"]
|
|
96
|
+
C5["erhua: 兒 outside a headed or tailed window → reject"]
|
|
97
|
+
C6["literary density > 0.05 → reject"]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
LX --> EV["Evidence — Taiwan lexicon, particles, institutions,<br/>有 + V perfective outside negation"]
|
|
101
|
+
EV --> R["Report{ok?, findings[], tier, han, evidence}"]
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Positive evidence never rejects. `了嗎` is not a defect: it is attested in every native
|
|
105
|
+
source measured. `有` as a perfective marker counts in favour, never against — but only
|
|
106
|
+
outside negation, since `沒有看` and `有沒有看` are pan-Mandarin and carry no Taiwan signal.
|
|
107
|
+
|
|
108
|
+
## Input and output
|
|
109
|
+
|
|
110
|
+
**Input** — anything responding to `to_s`. Empty, whitespace-only, Latin-only and
|
|
111
|
+
supplementary-plane input are reported, not raised on. Invalid byte sequences must be
|
|
112
|
+
scrubbed by the caller. There is no length limit; segmentation and normalization are
|
|
113
|
+
linear in input length.
|
|
114
|
+
|
|
115
|
+
**Output** — `TWFilter::Report`:
|
|
116
|
+
|
|
117
|
+
| member | meaning |
|
|
118
|
+
| --- | --- |
|
|
119
|
+
| `ok?` | true iff no finding has severity `:reject` |
|
|
120
|
+
| `findings` | every criterion that fired, in check order |
|
|
121
|
+
| `rejects`, `marks` | the two severity partitions; their union is `findings` |
|
|
122
|
+
| `tier` | highest MOE chart index reached: 0 常用, 1 次常用, 2 罕用, `nil` if unlisted |
|
|
123
|
+
| `han` | Han character count |
|
|
124
|
+
| `evidence` | count of positive Taiwan signals |
|
|
125
|
+
| `reasons` | `rejects` rendered as `"code: detail"` strings |
|
|
126
|
+
| `to_h` | plain data, JSON-serializable |
|
|
127
|
+
|
|
128
|
+
A `Finding` is `{check, code, severity, detail}`. Codes are stable across releases;
|
|
129
|
+
severity depends on the policy. Nothing is ever discarded silently — `keep?` is a
|
|
130
|
+
convenience over `examine`, not a separate path.
|
|
131
|
+
|
|
132
|
+
## Policies
|
|
133
|
+
|
|
134
|
+
Immutable value objects. Derive with `#with`.
|
|
135
|
+
|
|
136
|
+
| | `corpus` | `publishable` | `permissive` |
|
|
137
|
+
| --- | --- | --- | --- |
|
|
138
|
+
| han count | 6–60 | 6–40 | 1–∞ |
|
|
139
|
+
| han ratio | ≥ 0.65 | ≥ 0.70 | 0 |
|
|
140
|
+
| character tier | ≤ 罕用 | ≤ 常用 4 808 | ≤ 罕用 |
|
|
141
|
+
| soft lexical markers | mark | reject | mark |
|
|
142
|
+
| foreign topics | mark | reject | mark |
|
|
143
|
+
| punctuation inventory | wide | MOE strict | wide |
|
|
144
|
+
| converted orthography | reject | reject | reject |
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
TWFilter::Policy.corpus.with(max_tier: :secondary, han_range: (4..80))
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Usage
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
require "twfilter"
|
|
154
|
+
|
|
155
|
+
# Provenance, with reasons
|
|
156
|
+
report = TWFilter.examine("请把这个信息转发给同事")
|
|
157
|
+
report.ok? # => false
|
|
158
|
+
report.reasons # => ["simplified: 请这个转", "unlisted: 请这个转", "mainland: 信息"]
|
|
159
|
+
report.findings.map(&:code) # => [:simplified, :unlisted, :mainland]
|
|
160
|
+
|
|
161
|
+
# Editorial review rather than rejection
|
|
162
|
+
report = TWFilter.examine("這個程式的運行狀況非常穩定")
|
|
163
|
+
report.ok? # => true
|
|
164
|
+
report.marks.map(&:detail) # => ["運行"]
|
|
165
|
+
TWFilter::Checks::Lexicon.taiwan_form("運行") # => "執行"
|
|
166
|
+
|
|
167
|
+
# Pedagogical gate
|
|
168
|
+
TWFilter.keep?("他每天都會去公園散步運動。", policy: TWFilter::Policy.publishable) # => true
|
|
169
|
+
TWFilter.keep?("饕餮紋飾在青銅器上十分常見。", policy: TWFilter::Policy.publishable) # => false
|
|
170
|
+
|
|
171
|
+
# Punctuation to the Taiwan standard
|
|
172
|
+
TWFilter.normalize("他說“好”...") # => "他說「好」……"
|
|
173
|
+
TWFilter.normalize("馬丁·路德PDF") # => "馬丁‧路德PDF"
|
|
174
|
+
|
|
175
|
+
# Segmentation: 。!?…; terminate, closers stay attached, 、 does not break
|
|
176
|
+
TWFilter.sentences("他說:「好。」我走了。") # => ["他說:「好。」", "我走了。"]
|
|
177
|
+
|
|
178
|
+
# Character difficulty
|
|
179
|
+
TWFilter.tier("散步") # => 0
|
|
180
|
+
TWFilter.tier("饕餮") # => 1
|
|
181
|
+
|
|
182
|
+
# Block judgement — a paragraph is accepted as a unit or not at all
|
|
183
|
+
TWFilter::Block.judge(paragraph_sentences)
|
|
184
|
+
|
|
185
|
+
# Table fingerprint, to record beside derived measurements
|
|
186
|
+
TWFilter.provenance
|
|
187
|
+
# => {version: "1.0.0", dir: "…/lib/twfilter/data", tables: 19, digest: "14cd9fd7fc97dc9a"}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Placement in a pipeline
|
|
191
|
+
|
|
192
|
+
Normalize before segmenting: the ellipsis and dash rules change sentence boundaries.
|
|
193
|
+
Segment before examining: the shape thresholds are defined on sentences. Examine before
|
|
194
|
+
deduplicating and counting, so rejected material never reaches the statistics.
|
|
195
|
+
|
|
196
|
+
```text
|
|
197
|
+
acquire → normalize → segment → examine → deduplicate → tokenize → count
|
|
198
|
+
│ │ │
|
|
199
|
+
Punctuation Sentences Report
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`Block.judge` is an optional stage between segmentation and examination for sources whose
|
|
203
|
+
documents are topically coherent: it rejects a whole window when any member fails, and
|
|
204
|
+
requires a minimum density of positive evidence across the window. Use it on crawls, not
|
|
205
|
+
on dictionaries.
|
|
206
|
+
|
|
207
|
+
## Performance
|
|
208
|
+
|
|
209
|
+
Single core, Apple M5 Max, on 199 936 sentences of Taiwanese web text (6.2 M characters):
|
|
210
|
+
|
|
211
|
+
| operation | throughput | per 10⁷ sentences |
|
|
212
|
+
| --- | ---: | ---: |
|
|
213
|
+
| `Sentences.split` | 741 000 /s · 68 MB/s | 13 s |
|
|
214
|
+
| `Punctuation.normalize` | 67 000 /s · 6.1 MB/s | 149 s |
|
|
215
|
+
| `tier` | 134 000 /s · 12 MB/s | 75 s |
|
|
216
|
+
| `examine` (all five checks) | **10 700 /s · 1.0 MB/s** | 934 s |
|
|
217
|
+
|
|
218
|
+
Suitable for corpora of arbitrary size. Cost is linear in input length and dominated by
|
|
219
|
+
`Checks::Lexicon`, which scans 264 union-compiled patterns per span; the other four checks
|
|
220
|
+
together account for under a third of the total. Memory is constant — tables load once
|
|
221
|
+
(≈ 3 MB resident) and every entry point is a pure function over a string, so the work
|
|
222
|
+
parallelizes by `fork` or by thread without coordination.
|
|
223
|
+
|
|
224
|
+
A 37.7 M-sentence corpus takes ≈ 3.5 minutes of wall time across 18 cores. In practice
|
|
225
|
+
I/O, JSON parsing and deduplication dominate; this library is not usually the bottleneck.
|
|
226
|
+
|
|
227
|
+
## Reference tables
|
|
228
|
+
|
|
229
|
+
`lib/twfilter/data/` — plain text, one record per line, tab-separated where a record has
|
|
230
|
+
fields. Diffable, reviewable and versioned with the gem.
|
|
231
|
+
|
|
232
|
+
| table | rows | content |
|
|
233
|
+
| --- | ---: | --- |
|
|
234
|
+
| `moe_common/secondary/rare.txt` | 4 808 / 6 343 / 18 356 | 教育部標準字體表 |
|
|
235
|
+
| `moe_exception.txt` | 12 | dictionary headword characters outside all three charts |
|
|
236
|
+
| `simplified_only.txt` | 3 785 | derived from the OpenCC round-trip |
|
|
237
|
+
| `variants_used_in_taiwan.txt` | 16 | simplified-looking forms standard in Taiwan |
|
|
238
|
+
| `converted_orthography.txt` | 14 | mainland traditional variants |
|
|
239
|
+
| `mainland_hard.tsv` | 149 | rejecting pairs, with attestation counts |
|
|
240
|
+
| `mainland_soft.tsv` | 38 | marking pairs, ambiguous or dominated |
|
|
241
|
+
| `mainland_exceptions.tsv` | 6 | superstrings in which a marker is legitimate |
|
|
242
|
+
| `cantonese.txt` | 17 | written-Cantonese characters outside the 常用 chart |
|
|
243
|
+
| `regional_hard.tsv` | 19 | Hong Kong, Cantonese-sequence and Singapore forms |
|
|
244
|
+
| `foreign_topics.txt` | 80 | non-Taiwanese toponyms and institutions — subject matter, not provenance |
|
|
245
|
+
| `taiwan_markers/lexicon/particles/grammar.txt` | 66 / 51 / 7 / 20 | positive evidence |
|
|
246
|
+
| `erhua_headed/tailed.txt` | 14 / 36 | 兒 as a root morpheme |
|
|
247
|
+
| `wenyan.txt` | 4 | literary particles for density estimation |
|
|
248
|
+
| `MANIFEST.json` | — | row count and SHA-256 per table |
|
|
249
|
+
|
|
250
|
+
**Grading.** Candidate pairs are graded by attestation ratio against a reference corpus of
|
|
251
|
+
ten natively Taiwanese sources, 11 923 110 characters: *hard* when the mainland form is
|
|
252
|
+
unattested and the Taiwan form occurs at least three times; *soft* when the Taiwan form
|
|
253
|
+
dominates by 20 : 1 or the pair is hand-graded ambiguous; excluded otherwise. Most candidates
|
|
254
|
+
are excluded — conversion tables built for orthographic mapping contain many pairs where both
|
|
255
|
+
forms are ordinary Taiwanese.
|
|
256
|
+
|
|
257
|
+
The reference corpus admits no source with an open authorship gate. Open web comments on
|
|
258
|
+
Taiwanese channels are Taiwan-hosted but not Taiwan-authored: every mainland form attested
|
|
259
|
+
anywhere in the corpus occurs there and only there, so counting them would license the
|
|
260
|
+
mainland vocabulary of mainland commenters as Taiwanese.
|
|
261
|
+
|
|
262
|
+
**Membership.** Each table states the rule that admits a row, and `spec/twfilter/membership_spec.rb`
|
|
263
|
+
enforces it against the shipped data:
|
|
264
|
+
|
|
265
|
+
| table | a row is admitted iff |
|
|
266
|
+
| --- | --- |
|
|
267
|
+
| `cantonese.txt` | the character is used in written Cantonese and is not in 常用國字標準字體表 |
|
|
268
|
+
| `regional_hard.tsv` | the form is unattested in Taiwan and its Taiwan counterpart occurs ≥ 3 times |
|
|
269
|
+
| `converted_orthography.txt` | the form is in no MOE chart and the Taiwan form outnumbers it ≥ 20 : 1 |
|
|
270
|
+
| `variants_used_in_taiwan.txt` | the form looks simplified but is in an MOE chart |
|
|
271
|
+
| `mainland_hard.tsv` | the mainland form is unattested and the Taiwan form occurs ≥ 3 times |
|
|
272
|
+
| `mainland_soft.tsv` | the pair is a real opposition but the mainland form is attested here |
|
|
273
|
+
| `mainland_exceptions.tsv` | the head term is a detector in one of the two bands |
|
|
274
|
+
| `taiwan_lexicon.txt` | a mainland writer would not use this string for this referent |
|
|
275
|
+
| `taiwan_markers.txt` | the name denotes an ROC institution or a Taiwanese place |
|
|
276
|
+
| `taiwan_particles.txt` | the graph is the Taiwan-preferred spelling of the particle |
|
|
277
|
+
| `taiwan_grammar.txt` | the construction is a Southern Min substrate feature |
|
|
278
|
+
|
|
279
|
+
The rules exclude more than they admit. Subject matter is not provenance, so Taiwanese food
|
|
280
|
+
names are not evidence; A-not-A interrogatives are pan-Mandarin, so `好不好` is not evidence;
|
|
281
|
+
and a character in everyday Taiwanese use cannot mark another variety, however
|
|
282
|
+
characteristic of it that character may be.
|
|
283
|
+
|
|
284
|
+
**Topics are not provenance.** `foreign_topics.txt` is a subject-matter tag. Taiwanese media
|
|
285
|
+
covers the mainland constantly, several entries are ordinary ROC administrative vocabulary,
|
|
286
|
+
and Taipei streets are named after mainland cities. It marks under `corpus` and rejects only
|
|
287
|
+
under `publishable`, where excluding foreign subject matter is an editorial decision.
|
|
288
|
+
|
|
289
|
+
## Influencing the criteria
|
|
290
|
+
|
|
291
|
+
Preferred: contribute to the published dataset. The tables are maintained at
|
|
292
|
+
[huggingface.co/datasets/taiwan-corpora/twfilter-tables](https://huggingface.co/datasets/taiwan-corpora/twfilter-tables),
|
|
293
|
+
where each release carries its grading evidence, and the gem vendors a copy of a specific
|
|
294
|
+
version. A correction accepted there reaches every consumer at the next release, with a
|
|
295
|
+
record of what changed and why — which a local edit does not.
|
|
296
|
+
|
|
297
|
+
Three table sources are supported. All three are reported by `provenance`.
|
|
298
|
+
|
|
299
|
+
**Bundled** — the default, versioned with the gem, the only configuration whose results are
|
|
300
|
+
comparable with anyone else's.
|
|
301
|
+
|
|
302
|
+
**Replacement** — a directory holding the full set, e.g. a checkout of the dataset newer
|
|
303
|
+
than the gem:
|
|
304
|
+
|
|
305
|
+
```ruby
|
|
306
|
+
TWFilter::Tables.dir = "path/to/twfilter-tables" # or TWFILTER_DATA_DIR
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**Overlay** — the bundled set plus your additions. Filename suffixes select the operation:
|
|
310
|
+
|
|
311
|
+
| file in the overlay directory | effect on `mainland_hard.tsv` |
|
|
312
|
+
| --- | --- |
|
|
313
|
+
| `mainland_hard.add.tsv` | rows appended |
|
|
314
|
+
| `mainland_hard.remove.tsv` | rows removed, matched on the full row or its first field |
|
|
315
|
+
| `mainland_hard.tsv` | table replaced |
|
|
316
|
+
|
|
317
|
+
```ruby
|
|
318
|
+
TWFilter::Tables.overlay = "path/to/house-style" # or TWFILTER_OVERLAY_DIR
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Row format is validated on load and a violation raises `TWFilter::InvalidTableError`:
|
|
322
|
+
|
|
323
|
+
| shape | tables | row |
|
|
324
|
+
| --- | --- | --- |
|
|
325
|
+
| `:chars` | the MOE charts, `simplified_only`, `variants_used_in_taiwan`, `converted_orthography`, `cantonese`, `wenyan` | one Han character |
|
|
326
|
+
| `:pairs` | `mainland_hard`, `mainland_soft`, `mainland_exceptions` | two or more tab-separated fields, first non-empty |
|
|
327
|
+
| `:terms` | everything else, including tables you invent | one term, no tabs |
|
|
328
|
+
|
|
329
|
+
Files named `*.remove.*` carry keys only and are not shape-checked. UTF-8, one record per
|
|
330
|
+
line, no header, no comments; blank lines ignored.
|
|
331
|
+
|
|
332
|
+
Record `provenance` alongside any measurement derived this way. Two corpora filtered with
|
|
333
|
+
different tables are not comparable, and the digest is what makes that detectable.
|
|
334
|
+
|
|
335
|
+
Policy thresholds are the supported knob and need no table changes:
|
|
336
|
+
|
|
337
|
+
```ruby
|
|
338
|
+
lenient = TWFilter::Policy.corpus.with(soft_lexicon_rejects: false, foreign_topics_reject: false)
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Development
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
bundle install
|
|
345
|
+
bundle exec rake # specs and RBS validation
|
|
346
|
+
bundle exec rake tables # regenerate the tables and their manifest
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Specs require no data, no network and no credentials.
|
|
350
|
+
|
|
351
|
+
## License
|
|
352
|
+
|
|
353
|
+
Code: MIT. Tables: mixed, all permitting commercial use. See [NOTICES.md](NOTICES.md) for
|
|
354
|
+
per-table provenance and terms.
|
|
355
|
+
|
|
356
|
+
Two attribution strings must be reproduced by anything shipping this gem:
|
|
357
|
+
|
|
358
|
+
```text
|
|
359
|
+
數位發展部,CNS11643中文標準交換碼全字庫網站
|
|
360
|
+
OpenCC — https://github.com/BYVoid/OpenCC, Apache License 2.0
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
No table carries a NonCommercial, ShareAlike or copyleft term. Verdicts and counts produced
|
|
364
|
+
by the gem are the caller's, not derivatives of the tables.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TWFilter
|
|
4
|
+
module Block
|
|
5
|
+
# Sentences per judged window, accepted or rejected as a unit. 300 is large enough for
|
|
6
|
+
# the evidence rate to be stable and small enough to stay within one document.
|
|
7
|
+
SIZE = 300
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def judge(window, policy: Policy.corpus)
|
|
12
|
+
return [] if window.empty?
|
|
13
|
+
|
|
14
|
+
reports = window.map { |line| TWFilter.examine(line, policy: policy) }
|
|
15
|
+
return [] if reports.count { |report| !report.ok? } > policy.block_tolerance * window.length
|
|
16
|
+
return [] if Evidence.count(window.join) < policy.evidence_per_100 * window.length / 100
|
|
17
|
+
|
|
18
|
+
reports.filter_map { |report| report.text if report.ok? }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def each_kept(lines, size: SIZE, policy: Policy.corpus)
|
|
22
|
+
return to_enum(:each_kept, lines, size: size, policy: policy) unless block_given?
|
|
23
|
+
|
|
24
|
+
window = []
|
|
25
|
+
lines.each do |line|
|
|
26
|
+
window << line
|
|
27
|
+
next if window.length < size
|
|
28
|
+
|
|
29
|
+
judge(window, policy: policy).each { |kept| yield kept }
|
|
30
|
+
window = []
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
judge(window, policy: policy).each { |kept| yield kept } if window.any?
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TWFilter
|
|
4
|
+
module Checks
|
|
5
|
+
module Erhua
|
|
6
|
+
NAME = :erhua
|
|
7
|
+
# 兒 is a root morpheme inside a headed or tailed window, and an erhua suffix elsewhere.
|
|
8
|
+
MARK = "兒"
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def call(subject)
|
|
12
|
+
offender = offender(subject.text)
|
|
13
|
+
return [] if offender.nil?
|
|
14
|
+
|
|
15
|
+
[Finding.new(check: NAME, code: :erhua, detail: offender)]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def offender(text)
|
|
19
|
+
return nil if text == MARK
|
|
20
|
+
|
|
21
|
+
position = 0
|
|
22
|
+
while (position = text.index(MARK, position))
|
|
23
|
+
after = text[position, 2]
|
|
24
|
+
before = position.positive? ? text[position - 1, 2] : nil
|
|
25
|
+
|
|
26
|
+
return before || MARK unless headed.include?(after) || tailed.include?(before)
|
|
27
|
+
|
|
28
|
+
position += 1
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def headed = Tables.set("erhua_headed.txt")
|
|
35
|
+
|
|
36
|
+
def tailed = Tables.set("erhua_tailed.txt")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|