litsea 0.13.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/Cargo.toml +25 -0
- data/README.md +137 -0
- data/README_ja.md +137 -0
- data/extconf.rb +8 -0
- data/lib/litsea/version.rb +7 -0
- data/lib/litsea.rb +19 -0
- data/src/error.rs +120 -0
- data/src/gvl.rs +112 -0
- data/src/language.rs +31 -0
- data/src/lib.rs +84 -0
- data/src/metrics.rs +250 -0
- data/src/segmenter.rs +241 -0
- data/src/token.rs +135 -0
- data/src/trainer.rs +421 -0
- metadata +72 -0
data/src/lib.rs
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
//! Ruby binding for Litsea.
|
|
2
|
+
//!
|
|
3
|
+
//! Built with [magnus](https://github.com/matsadler/magnus). Everything
|
|
4
|
+
//! FFI-independent lives in `litsea-binding-core`, so this crate only maps
|
|
5
|
+
//! that surface onto Ruby classes and exceptions.
|
|
6
|
+
//!
|
|
7
|
+
//! Long-running work releases the GVL (see [`gvl`]), so other Ruby threads
|
|
8
|
+
//! keep running during training - which is what lets one of them cancel a
|
|
9
|
+
//! run that is already going.
|
|
10
|
+
//!
|
|
11
|
+
//! ```ruby
|
|
12
|
+
//! require "litsea"
|
|
13
|
+
//!
|
|
14
|
+
//! seg = Litsea::Segmenter.open("japanese", "models/japanese.model")
|
|
15
|
+
//! seg.segment("これはテストです。")
|
|
16
|
+
//! ```
|
|
17
|
+
|
|
18
|
+
pub mod error;
|
|
19
|
+
pub mod gvl;
|
|
20
|
+
pub mod language;
|
|
21
|
+
pub mod metrics;
|
|
22
|
+
pub mod segmenter;
|
|
23
|
+
pub mod token;
|
|
24
|
+
pub mod trainer;
|
|
25
|
+
|
|
26
|
+
use magnus::{Ruby, error::Error, function};
|
|
27
|
+
|
|
28
|
+
/// Returns the version of the underlying `litsea` crate.
|
|
29
|
+
///
|
|
30
|
+
/// # Returns
|
|
31
|
+
/// The version string, for example `"0.12.0"`.
|
|
32
|
+
fn version() -> String {
|
|
33
|
+
litsea::version().to_string()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Returns the names of every supported language.
|
|
37
|
+
///
|
|
38
|
+
/// # Returns
|
|
39
|
+
/// The canonical names, in documentation order.
|
|
40
|
+
fn supported_languages() -> Vec<String> {
|
|
41
|
+
litsea_binding_core::supported_language_names()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Entry point Ruby calls when the extension is required.
|
|
45
|
+
///
|
|
46
|
+
/// # Arguments
|
|
47
|
+
/// * `ruby` - The Ruby handle for the current thread.
|
|
48
|
+
///
|
|
49
|
+
/// # Returns
|
|
50
|
+
/// `()` once every class has been defined.
|
|
51
|
+
///
|
|
52
|
+
/// # Errors
|
|
53
|
+
/// Returns a Ruby exception if a class cannot be defined.
|
|
54
|
+
#[magnus::init]
|
|
55
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
56
|
+
let module = ruby.define_module("Litsea")?;
|
|
57
|
+
|
|
58
|
+
// Exceptions first: everything else raises through them.
|
|
59
|
+
error::define_exceptions(ruby, &module)?;
|
|
60
|
+
token::define(ruby, &module)?;
|
|
61
|
+
metrics::define(ruby, &module)?;
|
|
62
|
+
segmenter::define(ruby, &module)?;
|
|
63
|
+
trainer::define(ruby, &module)?;
|
|
64
|
+
|
|
65
|
+
module.define_module_function("version", function!(version, 0))?;
|
|
66
|
+
module.define_module_function("supported_languages", function!(supported_languages, 0))?;
|
|
67
|
+
|
|
68
|
+
Ok(())
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#[cfg(test)]
|
|
72
|
+
mod tests {
|
|
73
|
+
use super::*;
|
|
74
|
+
|
|
75
|
+
#[test]
|
|
76
|
+
fn test_version_matches_litsea() {
|
|
77
|
+
assert_eq!(version(), litsea::version());
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#[test]
|
|
81
|
+
fn test_supported_languages() {
|
|
82
|
+
assert_eq!(supported_languages(), vec!["japanese", "chinese", "korean", "english"]);
|
|
83
|
+
}
|
|
84
|
+
}
|
data/src/metrics.rs
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
//! Training metric classes.
|
|
2
|
+
|
|
3
|
+
use litsea::{BinaryMetrics, MulticlassMetrics, TwoStageMetrics};
|
|
4
|
+
use magnus::{Module, RModule, Ruby, error::Error};
|
|
5
|
+
|
|
6
|
+
/// Metrics from training a binary (segmentation) model.
|
|
7
|
+
///
|
|
8
|
+
/// All percentages are 0-100.
|
|
9
|
+
#[magnus::wrap(class = "Litsea::BinaryMetrics", free_immediately, size)]
|
|
10
|
+
pub struct RbBinaryMetrics {
|
|
11
|
+
/// The wrapped metrics.
|
|
12
|
+
inner: BinaryMetrics,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
impl RbBinaryMetrics {
|
|
16
|
+
/// Accuracy, as a percentage.
|
|
17
|
+
fn accuracy(&self) -> f64 {
|
|
18
|
+
self.inner.accuracy
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// Precision, as a percentage.
|
|
22
|
+
fn precision(&self) -> f64 {
|
|
23
|
+
self.inner.precision
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// Recall, as a percentage.
|
|
27
|
+
fn recall(&self) -> f64 {
|
|
28
|
+
self.inner.recall
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// Number of training instances.
|
|
32
|
+
fn num_instances(&self) -> usize {
|
|
33
|
+
self.inner.num_instances
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// True positives.
|
|
37
|
+
fn true_positives(&self) -> usize {
|
|
38
|
+
self.inner.true_positives
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// False positives.
|
|
42
|
+
fn false_positives(&self) -> usize {
|
|
43
|
+
self.inner.false_positives
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// False negatives.
|
|
47
|
+
fn false_negatives(&self) -> usize {
|
|
48
|
+
self.inner.false_negatives
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// True negatives.
|
|
52
|
+
fn true_negatives(&self) -> usize {
|
|
53
|
+
self.inner.true_negatives
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Returns a readable representation.
|
|
57
|
+
///
|
|
58
|
+
/// # Returns
|
|
59
|
+
/// For example `#<Litsea::BinaryMetrics accuracy=99.12% instances=1234>`.
|
|
60
|
+
fn inspect(&self) -> String {
|
|
61
|
+
format!(
|
|
62
|
+
"#<Litsea::BinaryMetrics accuracy={:.2}% instances={}>",
|
|
63
|
+
self.inner.accuracy, self.inner.num_instances
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
impl From<BinaryMetrics> for RbBinaryMetrics {
|
|
69
|
+
/// Wraps `litsea`'s metrics for Ruby.
|
|
70
|
+
///
|
|
71
|
+
/// # Arguments
|
|
72
|
+
/// * `inner` - The metrics to wrap.
|
|
73
|
+
///
|
|
74
|
+
/// # Returns
|
|
75
|
+
/// The corresponding [`RbBinaryMetrics`].
|
|
76
|
+
fn from(inner: BinaryMetrics) -> Self {
|
|
77
|
+
Self { inner }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// Metrics from training a multiclass model.
|
|
82
|
+
///
|
|
83
|
+
/// All percentages are 0-100.
|
|
84
|
+
#[magnus::wrap(class = "Litsea::MulticlassMetrics", free_immediately, size)]
|
|
85
|
+
pub struct RbMulticlassMetrics {
|
|
86
|
+
/// The wrapped metrics.
|
|
87
|
+
inner: MulticlassMetrics,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
impl RbMulticlassMetrics {
|
|
91
|
+
/// Accuracy, as a percentage.
|
|
92
|
+
fn accuracy(&self) -> f64 {
|
|
93
|
+
self.inner.accuracy
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Macro-averaged precision, as a percentage.
|
|
97
|
+
fn macro_precision(&self) -> f64 {
|
|
98
|
+
self.inner.macro_precision
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Macro-averaged recall, as a percentage.
|
|
102
|
+
fn macro_recall(&self) -> f64 {
|
|
103
|
+
self.inner.macro_recall
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// Number of training instances.
|
|
107
|
+
fn num_instances(&self) -> usize {
|
|
108
|
+
self.inner.num_instances
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// Gold instances per class, as a Hash keyed by the label name.
|
|
112
|
+
fn gold_per_class(&self) -> std::collections::HashMap<String, usize> {
|
|
113
|
+
self.inner.gold_per_class.clone()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// Correct predictions per class.
|
|
117
|
+
fn correct_per_class(&self) -> std::collections::HashMap<String, usize> {
|
|
118
|
+
self.inner.correct_per_class.clone()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Predictions made per class.
|
|
122
|
+
fn predicted_per_class(&self) -> std::collections::HashMap<String, usize> {
|
|
123
|
+
self.inner.predicted_per_class.clone()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/// Returns a readable representation.
|
|
127
|
+
///
|
|
128
|
+
/// # Returns
|
|
129
|
+
/// For example `#<Litsea::MulticlassMetrics accuracy=97.30% instances=999>`.
|
|
130
|
+
fn inspect(&self) -> String {
|
|
131
|
+
format!(
|
|
132
|
+
"#<Litsea::MulticlassMetrics accuracy={:.2}% instances={}>",
|
|
133
|
+
self.inner.accuracy, self.inner.num_instances
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
impl From<MulticlassMetrics> for RbMulticlassMetrics {
|
|
139
|
+
/// Wraps `litsea`'s metrics for Ruby.
|
|
140
|
+
///
|
|
141
|
+
/// # Arguments
|
|
142
|
+
/// * `inner` - The metrics to wrap.
|
|
143
|
+
///
|
|
144
|
+
/// # Returns
|
|
145
|
+
/// The corresponding [`RbMulticlassMetrics`].
|
|
146
|
+
fn from(inner: MulticlassMetrics) -> Self {
|
|
147
|
+
Self { inner }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// Metrics from training a two-stage model: one set per stage.
|
|
152
|
+
#[magnus::wrap(class = "Litsea::TwoStageMetrics", free_immediately, size)]
|
|
153
|
+
pub struct RbTwoStageMetrics {
|
|
154
|
+
/// The wrapped metrics.
|
|
155
|
+
inner: TwoStageMetrics,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
impl RbTwoStageMetrics {
|
|
159
|
+
/// Stage-1 (boundary classifier) metrics.
|
|
160
|
+
fn stage1(&self) -> RbMulticlassMetrics {
|
|
161
|
+
RbMulticlassMetrics::from(self.inner.stage1.clone())
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/// Stage-2 (word tagger) metrics.
|
|
165
|
+
fn stage2(&self) -> RbMulticlassMetrics {
|
|
166
|
+
RbMulticlassMetrics::from(self.inner.stage2.clone())
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/// Returns a readable representation.
|
|
170
|
+
///
|
|
171
|
+
/// # Returns
|
|
172
|
+
/// For example `#<Litsea::TwoStageMetrics stage1=99.10% stage2=95.40%>`.
|
|
173
|
+
fn inspect(&self) -> String {
|
|
174
|
+
format!(
|
|
175
|
+
"#<Litsea::TwoStageMetrics stage1={:.2}% stage2={:.2}%>",
|
|
176
|
+
self.inner.stage1.accuracy, self.inner.stage2.accuracy
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
impl From<TwoStageMetrics> for RbTwoStageMetrics {
|
|
182
|
+
/// Wraps `litsea`'s metrics for Ruby.
|
|
183
|
+
///
|
|
184
|
+
/// # Arguments
|
|
185
|
+
/// * `inner` - The metrics to wrap.
|
|
186
|
+
///
|
|
187
|
+
/// # Returns
|
|
188
|
+
/// The corresponding [`RbTwoStageMetrics`].
|
|
189
|
+
fn from(inner: TwoStageMetrics) -> Self {
|
|
190
|
+
Self { inner }
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/// Defines the metric classes.
|
|
195
|
+
///
|
|
196
|
+
/// # Arguments
|
|
197
|
+
/// * `ruby` - The Ruby handle for the current thread.
|
|
198
|
+
/// * `module` - The `Litsea` module to define the classes on.
|
|
199
|
+
///
|
|
200
|
+
/// # Returns
|
|
201
|
+
/// `()` on success.
|
|
202
|
+
///
|
|
203
|
+
/// # Errors
|
|
204
|
+
/// Returns a Ruby exception if a class cannot be defined.
|
|
205
|
+
pub fn define(ruby: &Ruby, module: &RModule) -> Result<(), Error> {
|
|
206
|
+
let binary = module.define_class("BinaryMetrics", ruby.class_object())?;
|
|
207
|
+
binary.define_method("accuracy", magnus::method!(RbBinaryMetrics::accuracy, 0))?;
|
|
208
|
+
binary.define_method("precision", magnus::method!(RbBinaryMetrics::precision, 0))?;
|
|
209
|
+
binary.define_method("recall", magnus::method!(RbBinaryMetrics::recall, 0))?;
|
|
210
|
+
binary.define_method("num_instances", magnus::method!(RbBinaryMetrics::num_instances, 0))?;
|
|
211
|
+
binary.define_method("true_positives", magnus::method!(RbBinaryMetrics::true_positives, 0))?;
|
|
212
|
+
binary
|
|
213
|
+
.define_method("false_positives", magnus::method!(RbBinaryMetrics::false_positives, 0))?;
|
|
214
|
+
binary
|
|
215
|
+
.define_method("false_negatives", magnus::method!(RbBinaryMetrics::false_negatives, 0))?;
|
|
216
|
+
binary.define_method("true_negatives", magnus::method!(RbBinaryMetrics::true_negatives, 0))?;
|
|
217
|
+
binary.define_method("inspect", magnus::method!(RbBinaryMetrics::inspect, 0))?;
|
|
218
|
+
binary.define_method("to_s", magnus::method!(RbBinaryMetrics::inspect, 0))?;
|
|
219
|
+
|
|
220
|
+
let multiclass = module.define_class("MulticlassMetrics", ruby.class_object())?;
|
|
221
|
+
multiclass.define_method("accuracy", magnus::method!(RbMulticlassMetrics::accuracy, 0))?;
|
|
222
|
+
multiclass.define_method(
|
|
223
|
+
"macro_precision",
|
|
224
|
+
magnus::method!(RbMulticlassMetrics::macro_precision, 0),
|
|
225
|
+
)?;
|
|
226
|
+
multiclass
|
|
227
|
+
.define_method("macro_recall", magnus::method!(RbMulticlassMetrics::macro_recall, 0))?;
|
|
228
|
+
multiclass
|
|
229
|
+
.define_method("num_instances", magnus::method!(RbMulticlassMetrics::num_instances, 0))?;
|
|
230
|
+
multiclass
|
|
231
|
+
.define_method("gold_per_class", magnus::method!(RbMulticlassMetrics::gold_per_class, 0))?;
|
|
232
|
+
multiclass.define_method(
|
|
233
|
+
"correct_per_class",
|
|
234
|
+
magnus::method!(RbMulticlassMetrics::correct_per_class, 0),
|
|
235
|
+
)?;
|
|
236
|
+
multiclass.define_method(
|
|
237
|
+
"predicted_per_class",
|
|
238
|
+
magnus::method!(RbMulticlassMetrics::predicted_per_class, 0),
|
|
239
|
+
)?;
|
|
240
|
+
multiclass.define_method("inspect", magnus::method!(RbMulticlassMetrics::inspect, 0))?;
|
|
241
|
+
multiclass.define_method("to_s", magnus::method!(RbMulticlassMetrics::inspect, 0))?;
|
|
242
|
+
|
|
243
|
+
let two_stage = module.define_class("TwoStageMetrics", ruby.class_object())?;
|
|
244
|
+
two_stage.define_method("stage1", magnus::method!(RbTwoStageMetrics::stage1, 0))?;
|
|
245
|
+
two_stage.define_method("stage2", magnus::method!(RbTwoStageMetrics::stage2, 0))?;
|
|
246
|
+
two_stage.define_method("inspect", magnus::method!(RbTwoStageMetrics::inspect, 0))?;
|
|
247
|
+
two_stage.define_method("to_s", magnus::method!(RbTwoStageMetrics::inspect, 0))?;
|
|
248
|
+
|
|
249
|
+
Ok(())
|
|
250
|
+
}
|
data/src/segmenter.rs
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
//! The `Litsea::Segmenter` class.
|
|
2
|
+
|
|
3
|
+
use std::path::Path;
|
|
4
|
+
|
|
5
|
+
use litsea_binding_core::{CoreSegmenter, TokenView};
|
|
6
|
+
use magnus::{Module, Object, RArray, RModule, RString, Ruby, Value, error::Error};
|
|
7
|
+
|
|
8
|
+
use crate::error::map_err;
|
|
9
|
+
use crate::gvl::without_gvl;
|
|
10
|
+
use crate::language::language_from_value;
|
|
11
|
+
use crate::token::Token;
|
|
12
|
+
|
|
13
|
+
/// A word segmenter, optionally with POS tagging.
|
|
14
|
+
///
|
|
15
|
+
/// Build one with `Segmenter.open`, `Segmenter.from_bytes`, or
|
|
16
|
+
/// `Segmenter.from_uri`. The kind of model is detected from the file itself,
|
|
17
|
+
/// so `has_pos?` describes what was loaded rather than something the caller
|
|
18
|
+
/// declares.
|
|
19
|
+
#[magnus::wrap(class = "Litsea::Segmenter", free_immediately, size)]
|
|
20
|
+
pub struct Segmenter {
|
|
21
|
+
/// The wrapped core segmenter.
|
|
22
|
+
inner: CoreSegmenter,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
impl Segmenter {
|
|
26
|
+
/// Loads a model from a filesystem path.
|
|
27
|
+
///
|
|
28
|
+
/// # Arguments
|
|
29
|
+
/// * `language` - A language name or ISO 639-1 code, as a String or Symbol.
|
|
30
|
+
/// * `path` - Path to the model file.
|
|
31
|
+
///
|
|
32
|
+
/// # Returns
|
|
33
|
+
/// The new segmenter.
|
|
34
|
+
///
|
|
35
|
+
/// # Errors
|
|
36
|
+
/// Raises `Litsea::InvalidArgumentError`, `Litsea::IoError`,
|
|
37
|
+
/// `Litsea::ParseError`, or `Litsea::ModelError`.
|
|
38
|
+
fn open(language: Value, path: String) -> Result<Self, Error> {
|
|
39
|
+
let language = language_from_value(language)?;
|
|
40
|
+
// Reading and compiling a model is measurable work; let other Ruby
|
|
41
|
+
// threads run while it happens.
|
|
42
|
+
let inner = without_gvl(|| CoreSegmenter::from_path(language, Path::new(&path)));
|
|
43
|
+
Ok(Self {
|
|
44
|
+
inner: map_err(inner)?,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// Loads a model from a raw byte string.
|
|
49
|
+
///
|
|
50
|
+
/// Takes the bytes as they are, so a String read with `File.binread`
|
|
51
|
+
/// (ASCII-8BIT) works as well as one read as UTF-8.
|
|
52
|
+
///
|
|
53
|
+
/// # Arguments
|
|
54
|
+
/// * `language` - A language name or ISO 639-1 code, as a String or Symbol.
|
|
55
|
+
/// * `data` - The model file contents.
|
|
56
|
+
///
|
|
57
|
+
/// # Returns
|
|
58
|
+
/// The new segmenter.
|
|
59
|
+
///
|
|
60
|
+
/// # Errors
|
|
61
|
+
/// Raises `Litsea::InvalidArgumentError`, `Litsea::ParseError`, or
|
|
62
|
+
/// `Litsea::ModelError`.
|
|
63
|
+
fn from_bytes(language: Value, data: RString) -> Result<Self, Error> {
|
|
64
|
+
let language = language_from_value(language)?;
|
|
65
|
+
// SAFETY: the slice is used only within this call, and nothing here
|
|
66
|
+
// runs Ruby code that could move or free the string in the meantime
|
|
67
|
+
// (`from_bytes` parses into owned Rust structures).
|
|
68
|
+
let bytes = unsafe { data.as_slice() };
|
|
69
|
+
Ok(Self {
|
|
70
|
+
inner: map_err(CoreSegmenter::from_bytes(language, bytes))?,
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Loads a model from a URI.
|
|
75
|
+
///
|
|
76
|
+
/// Accepts a filesystem path, a `file://` path, or an `http(s)://` URL.
|
|
77
|
+
/// The GVL is released while the model is fetched.
|
|
78
|
+
///
|
|
79
|
+
/// # Arguments
|
|
80
|
+
/// * `language` - A language name or ISO 639-1 code, as a String or Symbol.
|
|
81
|
+
/// * `uri` - The model URI.
|
|
82
|
+
///
|
|
83
|
+
/// # Returns
|
|
84
|
+
/// The new segmenter.
|
|
85
|
+
///
|
|
86
|
+
/// # Errors
|
|
87
|
+
/// Raises `Litsea::ModelError` if the download fails, plus the same
|
|
88
|
+
/// errors as `open`.
|
|
89
|
+
fn from_uri(language: Value, uri: String) -> Result<Self, Error> {
|
|
90
|
+
let language = language_from_value(language)?;
|
|
91
|
+
let inner = without_gvl(|| CoreSegmenter::from_uri_blocking(language, &uri));
|
|
92
|
+
Ok(Self {
|
|
93
|
+
inner: map_err(inner)?,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Returns the language this segmenter was built for.
|
|
98
|
+
///
|
|
99
|
+
/// # Returns
|
|
100
|
+
/// The canonical language name, for example `"japanese"`.
|
|
101
|
+
fn language(&self) -> String {
|
|
102
|
+
self.inner.language().to_string()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// Returns whether this segmenter can tag parts of speech.
|
|
106
|
+
///
|
|
107
|
+
/// # Returns
|
|
108
|
+
/// `true` when a two-stage POS model was loaded.
|
|
109
|
+
fn has_pos(&self) -> bool {
|
|
110
|
+
self.inner.has_pos()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// Splits a sentence into tokens.
|
|
114
|
+
///
|
|
115
|
+
/// # Arguments
|
|
116
|
+
/// * `text` - The sentence to segment.
|
|
117
|
+
///
|
|
118
|
+
/// # Returns
|
|
119
|
+
/// The tokens, in order.
|
|
120
|
+
fn segment(&self, text: String) -> Vec<String> {
|
|
121
|
+
self.inner.segment(&text)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// Splits several sentences into tokens, releasing the GVL.
|
|
125
|
+
///
|
|
126
|
+
/// # Arguments
|
|
127
|
+
/// * `texts` - The sentences to segment.
|
|
128
|
+
///
|
|
129
|
+
/// # Returns
|
|
130
|
+
/// One token array per input sentence, in input order.
|
|
131
|
+
fn segment_batch(&self, texts: Vec<String>) -> Vec<Vec<String>> {
|
|
132
|
+
without_gvl(|| self.inner.segment_batch(&texts))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// Splits a sentence into tokens carrying byte offsets.
|
|
136
|
+
///
|
|
137
|
+
/// # Arguments
|
|
138
|
+
/// * `ruby` - The Ruby handle for the current thread.
|
|
139
|
+
/// * `text` - The sentence to segment.
|
|
140
|
+
///
|
|
141
|
+
/// # Returns
|
|
142
|
+
/// The tokens, with `pos` set to `nil`.
|
|
143
|
+
///
|
|
144
|
+
/// # Errors
|
|
145
|
+
/// Returns a Ruby exception if the result array cannot be built.
|
|
146
|
+
fn segment_tokens(ruby: &Ruby, rb_self: &Self, text: String) -> Result<RArray, Error> {
|
|
147
|
+
tokens_to_array(ruby, rb_self.inner.segment_tokens(&text))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// Splits a sentence into tokens and tags each with a UPOS tag.
|
|
151
|
+
///
|
|
152
|
+
/// # Arguments
|
|
153
|
+
/// * `text` - The sentence to segment and tag.
|
|
154
|
+
///
|
|
155
|
+
/// # Returns
|
|
156
|
+
/// The tagged tokens, with byte offsets into `text`.
|
|
157
|
+
///
|
|
158
|
+
/// # Errors
|
|
159
|
+
/// Raises `Litsea::PosUnavailableError` when this segmenter was built
|
|
160
|
+
/// from a segmentation-only model.
|
|
161
|
+
fn segment_with_pos(ruby: &Ruby, rb_self: &Self, text: String) -> Result<RArray, Error> {
|
|
162
|
+
let tokens = map_err(rb_self.inner.segment_with_pos(&text))?;
|
|
163
|
+
tokens_to_array(ruby, tokens)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// Splits and tags several sentences, releasing the GVL.
|
|
167
|
+
///
|
|
168
|
+
/// # Arguments
|
|
169
|
+
/// * `texts` - The sentences to segment and tag.
|
|
170
|
+
///
|
|
171
|
+
/// # Returns
|
|
172
|
+
/// One tagged-token array per input sentence, in input order.
|
|
173
|
+
///
|
|
174
|
+
/// # Errors
|
|
175
|
+
/// Raises `Litsea::PosUnavailableError` when this segmenter was built
|
|
176
|
+
/// from a segmentation-only model.
|
|
177
|
+
fn segment_with_pos_batch(
|
|
178
|
+
ruby: &Ruby,
|
|
179
|
+
rb_self: &Self,
|
|
180
|
+
texts: Vec<String>,
|
|
181
|
+
) -> Result<RArray, Error> {
|
|
182
|
+
let batches = without_gvl(|| rb_self.inner.segment_with_pos_batch(&texts));
|
|
183
|
+
let outer = ruby.ary_new_capa(texts.len());
|
|
184
|
+
for tokens in map_err(batches)? {
|
|
185
|
+
outer.push(tokens_to_array(ruby, tokens)?)?;
|
|
186
|
+
}
|
|
187
|
+
Ok(outer)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// Builds a Ruby array of [`Token`] objects.
|
|
192
|
+
///
|
|
193
|
+
/// Wrapped types satisfy `IntoValue` but not `IntoValueFromNative`, which is
|
|
194
|
+
/// what `Vec<T>` conversion requires, so the array is built element by
|
|
195
|
+
/// element.
|
|
196
|
+
///
|
|
197
|
+
/// # Arguments
|
|
198
|
+
/// * `ruby` - The Ruby handle for the current thread.
|
|
199
|
+
/// * `tokens` - The token views to wrap.
|
|
200
|
+
///
|
|
201
|
+
/// # Returns
|
|
202
|
+
/// A Ruby array of `Litsea::Token` objects.
|
|
203
|
+
///
|
|
204
|
+
/// # Errors
|
|
205
|
+
/// Returns a Ruby exception if an element cannot be pushed.
|
|
206
|
+
fn tokens_to_array(ruby: &Ruby, tokens: Vec<TokenView>) -> Result<RArray, Error> {
|
|
207
|
+
let array = ruby.ary_new_capa(tokens.len());
|
|
208
|
+
for view in tokens {
|
|
209
|
+
array.push(Token::from(view))?;
|
|
210
|
+
}
|
|
211
|
+
Ok(array)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/// Defines `Litsea::Segmenter`.
|
|
215
|
+
///
|
|
216
|
+
/// # Arguments
|
|
217
|
+
/// * `ruby` - The Ruby handle for the current thread.
|
|
218
|
+
/// * `module` - The `Litsea` module to define the class on.
|
|
219
|
+
///
|
|
220
|
+
/// # Returns
|
|
221
|
+
/// `()` on success.
|
|
222
|
+
///
|
|
223
|
+
/// # Errors
|
|
224
|
+
/// Returns a Ruby exception if the class cannot be defined.
|
|
225
|
+
pub fn define(ruby: &Ruby, module: &RModule) -> Result<(), Error> {
|
|
226
|
+
let class = module.define_class("Segmenter", ruby.class_object())?;
|
|
227
|
+
class.define_singleton_method("open", magnus::function!(Segmenter::open, 2))?;
|
|
228
|
+
class.define_singleton_method("from_bytes", magnus::function!(Segmenter::from_bytes, 2))?;
|
|
229
|
+
class.define_singleton_method("from_uri", magnus::function!(Segmenter::from_uri, 2))?;
|
|
230
|
+
class.define_method("language", magnus::method!(Segmenter::language, 0))?;
|
|
231
|
+
class.define_method("has_pos?", magnus::method!(Segmenter::has_pos, 0))?;
|
|
232
|
+
class.define_method("segment", magnus::method!(Segmenter::segment, 1))?;
|
|
233
|
+
class.define_method("segment_batch", magnus::method!(Segmenter::segment_batch, 1))?;
|
|
234
|
+
class.define_method("segment_tokens", magnus::method!(Segmenter::segment_tokens, 1))?;
|
|
235
|
+
class.define_method("segment_with_pos", magnus::method!(Segmenter::segment_with_pos, 1))?;
|
|
236
|
+
class.define_method(
|
|
237
|
+
"segment_with_pos_batch",
|
|
238
|
+
magnus::method!(Segmenter::segment_with_pos_batch, 1),
|
|
239
|
+
)?;
|
|
240
|
+
Ok(())
|
|
241
|
+
}
|