litsea 0.14.3 → 0.15.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 +4 -4
- data/Cargo.toml +4 -4
- data/README.md +25 -3
- data/README_ja.md +25 -3
- data/lib/litsea/version.rb +1 -1
- data/src/segmenter.rs +119 -21
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 48bde143045210bc8ebc4dacba04155649772c88a97b88d4cbfbbf9d1f4670fa
|
|
4
|
+
data.tar.gz: d45ddd6b6b9875ab645cd5e20dc539974e189953ecd1c4851c5f90a5c6d4d76c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6bf4331b0bd88b94921d492429504761f5ec4a783c020080e9af6012d1e544fb7daae41b38e2580ebe1c3498bf9b49a6c0922c66c1355aef678821b4d2175865
|
|
7
|
+
data.tar.gz: a8b264e872e9e7a04601dd5f4e964893b0cfc3b2af2880b3328d7ad84c7ece3e499a7c615e972a66f78182f0522b62e5f372c5ce101a233b24f433c5085a883d
|
data/Cargo.toml
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
edition = "2024"
|
|
14
14
|
rust-version = "1.87"
|
|
15
15
|
name = "litsea-ruby"
|
|
16
|
-
version = "0.
|
|
16
|
+
version = "0.15.0"
|
|
17
17
|
build = false
|
|
18
18
|
autolib = false
|
|
19
19
|
autobins = false
|
|
@@ -44,14 +44,14 @@ crate-type = [
|
|
|
44
44
|
path = "src/lib.rs"
|
|
45
45
|
|
|
46
46
|
[dependencies.litsea]
|
|
47
|
-
version = "=0.
|
|
47
|
+
version = "=0.15.0"
|
|
48
48
|
|
|
49
49
|
[dependencies.litsea-binding-core]
|
|
50
|
-
version = "=0.
|
|
50
|
+
version = "=0.15.0"
|
|
51
51
|
features = ["remote_model"]
|
|
52
52
|
|
|
53
53
|
[dependencies.magnus]
|
|
54
|
-
version = "0.
|
|
54
|
+
version = "0.9.0"
|
|
55
55
|
features = ["rb-sys"]
|
|
56
56
|
|
|
57
57
|
[dependencies.rb-sys]
|
data/README.md
CHANGED
|
@@ -24,8 +24,8 @@ The gem ships code only. Download a pre-trained model from the [Litsea repositor
|
|
|
24
24
|
|
|
25
25
|
| Model | Purpose | Size |
|
|
26
26
|
|-------|---------|------|
|
|
27
|
-
| `japanese.model`, `chinese.model`, `korean.model`, `english.model` | Segmentation |
|
|
28
|
-
| `japanese_pos.model`, `chinese_pos.model`, `korean_pos.model`, `english_pos.model` | Segmentation + POS | 3.
|
|
27
|
+
| `japanese.model`, `chinese.model`, `korean.model`, `english.model` | Segmentation | 86 KB – 2.0 MB |
|
|
28
|
+
| `japanese_pos.model`, `chinese_pos.model`, `korean_pos.model`, `english_pos.model` | Segmentation + POS | 3.1 – 8.3 MB |
|
|
29
29
|
|
|
30
30
|
You never have to say which kind you have: the model file identifies itself, and `has_pos?` reports what the loaded model can do.
|
|
31
31
|
|
|
@@ -76,6 +76,28 @@ text.byteslice(token.start, token.end - token.start) # == token.surface
|
|
|
76
76
|
|
|
77
77
|
Calling `segment_with_pos` on a segmentation-only model raises `Litsea::PosUnavailableError`.
|
|
78
78
|
|
|
79
|
+
### User dictionary
|
|
80
|
+
|
|
81
|
+
Words passed as `user_words:` are emitted as single tokens wherever they occur, without retraining; every constructor accepts the keyword.
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
seg = Litsea::Segmenter.open(:japanese, "models/japanese.model", user_words: ["雪だるま", "新幹線"])
|
|
85
|
+
|
|
86
|
+
seg.segment("雪だるまを作る")
|
|
87
|
+
# => ["雪だるま", "を", "作る"] (without the dictionary: ["雪", "だる", "ま", "を", "作る"])
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Matching is context-free, so a short fragment registered as a word breaks every other word it occurs in. `user_words_gate:` (`0.0`-`1.0`) applies a word only where the model's margin against it is small enough: `0.0` only where the model already agrees with it.
|
|
91
|
+
|
|
92
|
+
With a POS model, `user_word_tags:` (a Hash of words and UPOS tags, as Strings) registers further words whose tokens `segment_with_pos` gives the registered tag, ahead of the model's own tagging:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
tagged = Litsea::Segmenter.open(:japanese, "models/japanese_pos.model", user_word_tags: { "チャットGPT" => "PROPN" })
|
|
96
|
+
|
|
97
|
+
tagged.segment_with_pos("チャットGPTに聞く").map { |t| "#{t.surface}/#{t.pos}" }
|
|
98
|
+
# => ["チャットGPT/PROPN", "に/ADP", "聞く/VERB"] (in user_words: instead: "チャットGPT/NOUN")
|
|
99
|
+
```
|
|
100
|
+
|
|
79
101
|
### Other model sources
|
|
80
102
|
|
|
81
103
|
```ruby
|
|
@@ -122,7 +144,7 @@ Every error derives from `Litsea::Error`, so one `rescue` handles them all.
|
|
|
122
144
|
|
|
123
145
|
| Error | Raised when |
|
|
124
146
|
|-------|-------------|
|
|
125
|
-
| `Litsea::InvalidArgumentError` | Unknown language name, unknown feature set, reused trainer |
|
|
147
|
+
| `Litsea::InvalidArgumentError` | Unknown language name, unknown feature set, reused trainer, `user_words_gate:` outside `0.0`-`1.0`, a `user_word_tags:` value that is not a UPOS tag |
|
|
126
148
|
| `Litsea::ModelError` | Download failed, or the file is a legacy joint POS model |
|
|
127
149
|
| `Litsea::IoError` | A file could not be read or written |
|
|
128
150
|
| `Litsea::ParseError` | The model or training data is malformed |
|
data/README_ja.md
CHANGED
|
@@ -24,8 +24,8 @@ gem にはコードのみが含まれます。事前学習済みモデルは [Li
|
|
|
24
24
|
|
|
25
25
|
| モデル | 用途 | サイズ |
|
|
26
26
|
|-------|------|-------|
|
|
27
|
-
| `japanese.model`, `chinese.model`, `korean.model`, `english.model` | 分割 |
|
|
28
|
-
| `japanese_pos.model`, `chinese_pos.model`, `korean_pos.model`, `english_pos.model` | 分割 + POS | 3.
|
|
27
|
+
| `japanese.model`, `chinese.model`, `korean.model`, `english.model` | 分割 | 86KB〜2.0MB |
|
|
28
|
+
| `japanese_pos.model`, `chinese_pos.model`, `korean_pos.model`, `english_pos.model` | 分割 + POS | 3.1〜8.3MB |
|
|
29
29
|
|
|
30
30
|
どちらの種別かを指定する必要はありません。モデルファイル自身が種別を持っており、読み込んだモデルで何ができるかは `has_pos?` が示します。
|
|
31
31
|
|
|
@@ -76,6 +76,28 @@ text.byteslice(token.start, token.end - token.start) # == token.surface
|
|
|
76
76
|
|
|
77
77
|
分割専用モデルに対して `segment_with_pos` を呼ぶと `Litsea::PosUnavailableError` が発生します。
|
|
78
78
|
|
|
79
|
+
### ユーザー辞書
|
|
80
|
+
|
|
81
|
+
`user_words:` に渡した語は、再学習なしで、現れた場所で必ず 1 トークンとして出力されます。すべてのコンストラクタがこのキーワードを受け取ります。
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
seg = Litsea::Segmenter.open(:japanese, "models/japanese.model", user_words: ["雪だるま", "新幹線"])
|
|
85
|
+
|
|
86
|
+
seg.segment("雪だるまを作る")
|
|
87
|
+
# => ["雪だるま", "を", "作る"] (辞書なしでは ["雪", "だる", "ま", "を", "作る"])
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
照合は文脈を見ないため、短い断片を語として登録すると、それを含む他の語をすべて壊します。`user_words_gate:`(`0.0`〜`1.0`)は、モデルの margin が十分小さい場所にだけ語を適用します。`0.0` では、モデルがすでに同意している場所だけに適用します。
|
|
91
|
+
|
|
92
|
+
POS モデルでは、`user_word_tags:`(語と UPOS タグの Hash。キーと値は String)で、さらに語を登録できます。`segment_with_pos` は、その語のトークンに、モデル自身のタグ付けより優先して、登録したタグを付けます:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
tagged = Litsea::Segmenter.open(:japanese, "models/japanese_pos.model", user_word_tags: { "チャットGPT" => "PROPN" })
|
|
96
|
+
|
|
97
|
+
tagged.segment_with_pos("チャットGPTに聞く").map { |t| "#{t.surface}/#{t.pos}" }
|
|
98
|
+
# => ["チャットGPT/PROPN", "に/ADP", "聞く/VERB"] (user_words: に入れた場合は "チャットGPT/NOUN")
|
|
99
|
+
```
|
|
100
|
+
|
|
79
101
|
### その他のモデル読み込み方法
|
|
80
102
|
|
|
81
103
|
```ruby
|
|
@@ -122,7 +144,7 @@ metrics = Litsea::Trainer.new(0.01, 100_000, "features.txt").train("japanese.mod
|
|
|
122
144
|
|
|
123
145
|
| エラー | 発生条件 |
|
|
124
146
|
|-------|---------|
|
|
125
|
-
| `Litsea::InvalidArgumentError` | 未知の言語名、未知の feature set
|
|
147
|
+
| `Litsea::InvalidArgumentError` | 未知の言語名、未知の feature set、使用済みトレーナ、`0.0`〜`1.0` の外の `user_words_gate:`、UPOS タグでない `user_word_tags:` の値 |
|
|
126
148
|
| `Litsea::ModelError` | ダウンロード失敗、または旧 joint POS モデル |
|
|
127
149
|
| `Litsea::IoError` | ファイルの読み書き失敗 |
|
|
128
150
|
| `Litsea::ParseError` | モデルまたは学習データの形式不正 |
|
data/lib/litsea/version.rb
CHANGED
data/src/segmenter.rs
CHANGED
|
@@ -1,21 +1,106 @@
|
|
|
1
1
|
//! The `Litsea::Segmenter` class.
|
|
2
2
|
|
|
3
|
+
use std::collections::HashMap;
|
|
3
4
|
use std::path::Path;
|
|
4
5
|
|
|
5
|
-
use
|
|
6
|
-
use
|
|
6
|
+
use litsea::UserDictionary;
|
|
7
|
+
use litsea_binding_core::{CoreSegmenter, TokenView, build_user_dictionary_with_tags};
|
|
8
|
+
use magnus::scan_args::{get_kwargs, scan_args};
|
|
9
|
+
use magnus::{Module, Object, RArray, RModule, RString, Ruby, TryConvert, Value, error::Error};
|
|
7
10
|
|
|
8
11
|
use crate::error::map_err;
|
|
9
12
|
use crate::gvl::without_gvl;
|
|
10
13
|
use crate::language::language_from_value;
|
|
11
14
|
use crate::token::Token;
|
|
12
15
|
|
|
16
|
+
/// The keyword arguments every constructor accepts (issues #263, #264): the
|
|
17
|
+
/// optional `user_words:`, `user_words_gate:` and `user_word_tags:`, each of
|
|
18
|
+
/// which may also be passed as `nil`.
|
|
19
|
+
struct UserWordsArgs {
|
|
20
|
+
/// The words to emit as single tokens.
|
|
21
|
+
user_words: Option<Vec<String>>,
|
|
22
|
+
/// The flip-ratio gate in `0.0..=1.0`.
|
|
23
|
+
user_words_gate: Option<f64>,
|
|
24
|
+
/// Words to emit as single tokens with the UPOS tag `segment_with_pos`
|
|
25
|
+
/// gives them.
|
|
26
|
+
user_word_tags: Option<HashMap<String, String>>,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
impl UserWordsArgs {
|
|
30
|
+
/// Builds the user dictionary these arguments describe.
|
|
31
|
+
///
|
|
32
|
+
/// # Returns
|
|
33
|
+
/// `None` when no words were given, otherwise the dictionary.
|
|
34
|
+
///
|
|
35
|
+
/// # Errors
|
|
36
|
+
/// Raises `Litsea::InvalidArgumentError` if a tag is not one of the 17
|
|
37
|
+
/// UPOS tags, or if the gate is outside `0.0..=1.0`, also when no words
|
|
38
|
+
/// were given.
|
|
39
|
+
fn dictionary(self) -> Result<Option<UserDictionary>, Error> {
|
|
40
|
+
let word_tags: Vec<(String, String)> =
|
|
41
|
+
self.user_word_tags.unwrap_or_default().into_iter().collect();
|
|
42
|
+
map_err(build_user_dictionary_with_tags(
|
|
43
|
+
self.user_words.as_deref().unwrap_or_default(),
|
|
44
|
+
&word_tags,
|
|
45
|
+
self.user_words_gate,
|
|
46
|
+
))
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Splits the arguments of `open`, `from_bytes`, and `from_uri`: the
|
|
51
|
+
/// language and the model source as required positionals, then the
|
|
52
|
+
/// `user_words:` / `user_words_gate:` / `user_word_tags:` keywords.
|
|
53
|
+
///
|
|
54
|
+
/// magnus cannot express optional keyword arguments in a `function!`
|
|
55
|
+
/// arity, so the constructors take their arguments through `scan_args`,
|
|
56
|
+
/// as the trainers do.
|
|
57
|
+
///
|
|
58
|
+
/// # Arguments
|
|
59
|
+
/// * `args` - The raw Ruby arguments.
|
|
60
|
+
///
|
|
61
|
+
/// # Returns
|
|
62
|
+
/// The language value, the model source, and the keyword arguments.
|
|
63
|
+
///
|
|
64
|
+
/// # Errors
|
|
65
|
+
/// Returns a Ruby `ArgumentError` for a wrong argument count or an unknown
|
|
66
|
+
/// keyword, or a `TypeError` if an argument has the wrong type.
|
|
67
|
+
fn scan_constructor_args<T: TryConvert>(
|
|
68
|
+
args: &[Value],
|
|
69
|
+
) -> Result<(Value, T, UserWordsArgs), Error> {
|
|
70
|
+
let args = scan_args::<(Value, T), (), (), (), _, ()>(args)?;
|
|
71
|
+
let (language, source) = args.required;
|
|
72
|
+
let kwargs = get_kwargs::<
|
|
73
|
+
_,
|
|
74
|
+
(),
|
|
75
|
+
(
|
|
76
|
+
Option<Option<Vec<String>>>,
|
|
77
|
+
Option<Option<f64>>,
|
|
78
|
+
Option<Option<HashMap<String, String>>>,
|
|
79
|
+
),
|
|
80
|
+
(),
|
|
81
|
+
>(args.keywords, &[], &["user_words", "user_words_gate", "user_word_tags"])?;
|
|
82
|
+
let (user_words, user_words_gate, user_word_tags) = kwargs.optional;
|
|
83
|
+
Ok((
|
|
84
|
+
language,
|
|
85
|
+
source,
|
|
86
|
+
UserWordsArgs {
|
|
87
|
+
user_words: user_words.flatten(),
|
|
88
|
+
user_words_gate: user_words_gate.flatten(),
|
|
89
|
+
user_word_tags: user_word_tags.flatten(),
|
|
90
|
+
},
|
|
91
|
+
))
|
|
92
|
+
}
|
|
93
|
+
|
|
13
94
|
/// A word segmenter, optionally with POS tagging.
|
|
14
95
|
///
|
|
15
96
|
/// Build one with `Segmenter.open`, `Segmenter.from_bytes`, or
|
|
16
97
|
/// `Segmenter.from_uri`. The kind of model is detected from the file itself,
|
|
17
98
|
/// so `has_pos?` describes what was loaded rather than something the caller
|
|
18
|
-
/// declares.
|
|
99
|
+
/// declares. Every constructor accepts the keyword arguments `user_words:`,
|
|
100
|
+
/// an array of words to emit as single tokens; `user_words_gate:`, a flip
|
|
101
|
+
/// ratio in `0.0..=1.0` that applies a word only where the model's margin
|
|
102
|
+
/// against it is small enough; and `user_word_tags:`, a Hash mapping further
|
|
103
|
+
/// words to the UPOS tag `segment_with_pos` gives them.
|
|
19
104
|
#[magnus::wrap(class = "Litsea::Segmenter", free_immediately, size)]
|
|
20
105
|
pub struct Segmenter {
|
|
21
106
|
/// The wrapped core segmenter.
|
|
@@ -26,20 +111,25 @@ impl Segmenter {
|
|
|
26
111
|
/// Loads a model from a filesystem path.
|
|
27
112
|
///
|
|
28
113
|
/// # Arguments
|
|
29
|
-
/// * `
|
|
30
|
-
///
|
|
114
|
+
/// * `args` - `language` (a language name or ISO 639-1 code, as a String
|
|
115
|
+
/// or Symbol), `path` (the model file), and the optional
|
|
116
|
+
/// `user_words:` / `user_words_gate:` / `user_word_tags:` keywords.
|
|
31
117
|
///
|
|
32
118
|
/// # Returns
|
|
33
119
|
/// The new segmenter.
|
|
34
120
|
///
|
|
35
121
|
/// # Errors
|
|
36
|
-
/// Raises `Litsea::InvalidArgumentError
|
|
122
|
+
/// Raises `Litsea::InvalidArgumentError` (also for a tag that is not a
|
|
123
|
+
/// UPOS tag or a gate outside `0.0..=1.0`), `Litsea::IoError`,
|
|
37
124
|
/// `Litsea::ParseError`, or `Litsea::ModelError`.
|
|
38
|
-
fn open(
|
|
125
|
+
fn open(args: &[Value]) -> Result<Self, Error> {
|
|
126
|
+
let (language, path, user_words): (_, String, _) = scan_constructor_args(args)?;
|
|
39
127
|
let language = language_from_value(language)?;
|
|
128
|
+
let dictionary = user_words.dictionary()?;
|
|
40
129
|
// Reading and compiling a model is measurable work; let other Ruby
|
|
41
130
|
// threads run while it happens.
|
|
42
|
-
let inner =
|
|
131
|
+
let inner =
|
|
132
|
+
without_gvl(|| CoreSegmenter::from_path_with(language, Path::new(&path), dictionary));
|
|
43
133
|
Ok(Self {
|
|
44
134
|
inner: map_err(inner)?,
|
|
45
135
|
})
|
|
@@ -51,23 +141,27 @@ impl Segmenter {
|
|
|
51
141
|
/// (ASCII-8BIT) works as well as one read as UTF-8.
|
|
52
142
|
///
|
|
53
143
|
/// # Arguments
|
|
54
|
-
/// * `
|
|
55
|
-
///
|
|
144
|
+
/// * `args` - `language` (a language name or ISO 639-1 code, as a String
|
|
145
|
+
/// or Symbol), `data` (the model file contents), and the optional
|
|
146
|
+
/// `user_words:` / `user_words_gate:` / `user_word_tags:` keywords.
|
|
56
147
|
///
|
|
57
148
|
/// # Returns
|
|
58
149
|
/// The new segmenter.
|
|
59
150
|
///
|
|
60
151
|
/// # Errors
|
|
61
|
-
/// Raises `Litsea::InvalidArgumentError
|
|
152
|
+
/// Raises `Litsea::InvalidArgumentError` (also for a tag that is not a
|
|
153
|
+
/// UPOS tag or a gate outside `0.0..=1.0`), `Litsea::ParseError`, or
|
|
62
154
|
/// `Litsea::ModelError`.
|
|
63
|
-
fn from_bytes(
|
|
155
|
+
fn from_bytes(args: &[Value]) -> Result<Self, Error> {
|
|
156
|
+
let (language, data, user_words): (_, RString, _) = scan_constructor_args(args)?;
|
|
64
157
|
let language = language_from_value(language)?;
|
|
158
|
+
let dictionary = user_words.dictionary()?;
|
|
65
159
|
// SAFETY: the slice is used only within this call, and nothing here
|
|
66
160
|
// runs Ruby code that could move or free the string in the meantime
|
|
67
|
-
// (`
|
|
161
|
+
// (`from_bytes_with` parses into owned Rust structures).
|
|
68
162
|
let bytes = unsafe { data.as_slice() };
|
|
69
163
|
Ok(Self {
|
|
70
|
-
inner: map_err(CoreSegmenter::
|
|
164
|
+
inner: map_err(CoreSegmenter::from_bytes_with(language, bytes, dictionary))?,
|
|
71
165
|
})
|
|
72
166
|
}
|
|
73
167
|
|
|
@@ -77,8 +171,9 @@ impl Segmenter {
|
|
|
77
171
|
/// The GVL is released while the model is fetched.
|
|
78
172
|
///
|
|
79
173
|
/// # Arguments
|
|
80
|
-
/// * `
|
|
81
|
-
///
|
|
174
|
+
/// * `args` - `language` (a language name or ISO 639-1 code, as a String
|
|
175
|
+
/// or Symbol), `uri` (the model URI), and the optional `user_words:` /
|
|
176
|
+
/// `user_words_gate:` / `user_word_tags:` keywords.
|
|
82
177
|
///
|
|
83
178
|
/// # Returns
|
|
84
179
|
/// The new segmenter.
|
|
@@ -86,9 +181,12 @@ impl Segmenter {
|
|
|
86
181
|
/// # Errors
|
|
87
182
|
/// Raises `Litsea::ModelError` if the download fails, plus the same
|
|
88
183
|
/// errors as `open`.
|
|
89
|
-
fn from_uri(
|
|
184
|
+
fn from_uri(args: &[Value]) -> Result<Self, Error> {
|
|
185
|
+
let (language, uri, user_words): (_, String, _) = scan_constructor_args(args)?;
|
|
90
186
|
let language = language_from_value(language)?;
|
|
91
|
-
let
|
|
187
|
+
let dictionary = user_words.dictionary()?;
|
|
188
|
+
let inner =
|
|
189
|
+
without_gvl(|| CoreSegmenter::from_uri_blocking_with(language, &uri, dictionary));
|
|
92
190
|
Ok(Self {
|
|
93
191
|
inner: map_err(inner)?,
|
|
94
192
|
})
|
|
@@ -224,9 +322,9 @@ fn tokens_to_array(ruby: &Ruby, tokens: Vec<TokenView>) -> Result<RArray, Error>
|
|
|
224
322
|
/// Returns a Ruby exception if the class cannot be defined.
|
|
225
323
|
pub fn define(ruby: &Ruby, module: &RModule) -> Result<(), Error> {
|
|
226
324
|
let class = module.define_class("Segmenter", ruby.class_object())?;
|
|
227
|
-
class.define_singleton_method("open", magnus::function!(Segmenter::open,
|
|
228
|
-
class.define_singleton_method("from_bytes", magnus::function!(Segmenter::from_bytes,
|
|
229
|
-
class.define_singleton_method("from_uri", magnus::function!(Segmenter::from_uri,
|
|
325
|
+
class.define_singleton_method("open", magnus::function!(Segmenter::open, -1))?;
|
|
326
|
+
class.define_singleton_method("from_bytes", magnus::function!(Segmenter::from_bytes, -1))?;
|
|
327
|
+
class.define_singleton_method("from_uri", magnus::function!(Segmenter::from_uri, -1))?;
|
|
230
328
|
class.define_method("language", magnus::method!(Segmenter::language, 0))?;
|
|
231
329
|
class.define_method("has_pos?", magnus::method!(Segmenter::has_pos, 0))?;
|
|
232
330
|
class.define_method("segment", magnus::method!(Segmenter::segment, 1))?;
|