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.
data/src/token.rs ADDED
@@ -0,0 +1,135 @@
1
+ //! The token class handed to Ruby.
2
+
3
+ use litsea_binding_core::TokenView;
4
+ use magnus::{Module, RModule, Ruby, error::Error};
5
+
6
+ /// A segmented token.
7
+ ///
8
+ /// `start` and `end` are byte offsets into the input string, so
9
+ /// `text.byteslice(token.start, token.end - token.start)` returns the
10
+ /// surface. Ruby's `String#[]` works in characters, hence `byteslice`.
11
+ #[magnus::wrap(class = "Litsea::Token", free_immediately, size)]
12
+ pub struct Token {
13
+ /// The token's surface form.
14
+ surface: String,
15
+ /// The UPOS tag name (for example `"NOUN"`), or `nil` when the segmenter
16
+ /// has no POS model.
17
+ pos: Option<String>,
18
+ /// Starting byte offset in the input string.
19
+ start: usize,
20
+ /// Ending byte offset (exclusive) in the input string.
21
+ end: usize,
22
+ }
23
+
24
+ impl Token {
25
+ /// Returns the token's surface form.
26
+ ///
27
+ /// # Returns
28
+ /// The surface string.
29
+ fn surface(&self) -> String {
30
+ self.surface.clone()
31
+ }
32
+
33
+ /// Returns the UPOS tag name.
34
+ ///
35
+ /// # Returns
36
+ /// The tag name, or `nil` for segmentation-only output.
37
+ fn pos(&self) -> Option<String> {
38
+ self.pos.clone()
39
+ }
40
+
41
+ /// Returns the starting byte offset.
42
+ ///
43
+ /// # Returns
44
+ /// The offset into the input string.
45
+ fn start(&self) -> usize {
46
+ self.start
47
+ }
48
+
49
+ /// Returns the ending byte offset (exclusive).
50
+ ///
51
+ /// # Returns
52
+ /// The offset into the input string.
53
+ fn end(&self) -> usize {
54
+ self.end
55
+ }
56
+
57
+ /// Returns a readable representation.
58
+ ///
59
+ /// # Returns
60
+ /// For example `#<Litsea::Token これ/PRON [0..6]>`.
61
+ fn inspect(&self) -> String {
62
+ match &self.pos {
63
+ Some(pos) => {
64
+ format!("#<Litsea::Token {}/{} [{}..{}]>", self.surface, pos, self.start, self.end)
65
+ }
66
+ None => format!("#<Litsea::Token {} [{}..{}]>", self.surface, self.start, self.end),
67
+ }
68
+ }
69
+ }
70
+
71
+ impl From<TokenView> for Token {
72
+ /// Converts a core token view into the Ruby-facing token.
73
+ ///
74
+ /// # Arguments
75
+ /// * `view` - The token view.
76
+ ///
77
+ /// # Returns
78
+ /// The corresponding [`Token`].
79
+ fn from(view: TokenView) -> Self {
80
+ Self {
81
+ surface: view.surface,
82
+ // The tag travels as its name, matching the other bindings'
83
+ // token shape.
84
+ pos: view.pos.map(|pos| pos.to_string()),
85
+ start: view.byte_start,
86
+ end: view.byte_end,
87
+ }
88
+ }
89
+ }
90
+
91
+ /// Defines `Litsea::Token`.
92
+ ///
93
+ /// # Arguments
94
+ /// * `ruby` - The Ruby handle for the current thread.
95
+ /// * `module` - The `Litsea` module to define the class on.
96
+ ///
97
+ /// # Returns
98
+ /// `()` on success.
99
+ ///
100
+ /// # Errors
101
+ /// Returns a Ruby exception if the class cannot be defined.
102
+ pub fn define(ruby: &Ruby, module: &RModule) -> Result<(), Error> {
103
+ let class = module.define_class("Token", ruby.class_object())?;
104
+ class.define_method("surface", magnus::method!(Token::surface, 0))?;
105
+ class.define_method("pos", magnus::method!(Token::pos, 0))?;
106
+ class.define_method("start", magnus::method!(Token::start, 0))?;
107
+ class.define_method("end", magnus::method!(Token::end, 0))?;
108
+ class.define_method("inspect", magnus::method!(Token::inspect, 0))?;
109
+ class.define_method("to_s", magnus::method!(Token::inspect, 0))?;
110
+ Ok(())
111
+ }
112
+
113
+ #[cfg(test)]
114
+ mod tests {
115
+ use litsea::Upos;
116
+
117
+ use super::*;
118
+
119
+ #[test]
120
+ fn test_conversion_keeps_offsets_and_tag_name() {
121
+ let token = Token::from(TokenView::new("テスト", 9, 18, Some(Upos::NOUN)));
122
+ assert_eq!(token.surface(), "テスト");
123
+ assert_eq!(token.pos().as_deref(), Some("NOUN"));
124
+ assert_eq!(token.start(), 9);
125
+ assert_eq!(token.end(), 18);
126
+ assert_eq!(token.inspect(), "#<Litsea::Token テスト/NOUN [9..18]>");
127
+ }
128
+
129
+ #[test]
130
+ fn test_untagged_token() {
131
+ let token = Token::from(TokenView::new("テスト", 0, 9, None));
132
+ assert_eq!(token.pos(), None);
133
+ assert_eq!(token.inspect(), "#<Litsea::Token テスト [0..9]>");
134
+ }
135
+ }
data/src/trainer.rs ADDED
@@ -0,0 +1,421 @@
1
+ //! Feature extraction, training, and cancellation.
2
+ //!
3
+ //! Training releases the GVL (see [`crate::gvl`]), so other Ruby threads keep
4
+ //! running while it works - which is what makes [`CancelToken`] usable while
5
+ //! a run is already going, as in the Python and Node.js bindings.
6
+
7
+ use std::cell::RefCell;
8
+ use std::path::PathBuf;
9
+
10
+ use litsea_binding_core::{
11
+ CancelToken as CoreCancelToken, CoreExtractor, CorePerceptronTrainer, CoreTrainer,
12
+ CoreTwoStageTrainer, CorpusFormat, parse_feature_set,
13
+ };
14
+ use magnus::{
15
+ Module, Object, RModule, Ruby, Value, error::Error, function, method, scan_args::scan_args,
16
+ };
17
+
18
+ use crate::error::map_err;
19
+ use crate::gvl::without_gvl;
20
+ use crate::language::language_from_value;
21
+ use crate::metrics::{RbBinaryMetrics, RbMulticlassMetrics, RbTwoStageMetrics};
22
+
23
+ /// A flag that asks a running training job to stop.
24
+ ///
25
+ /// Cancelling is cooperative and is **not** an error: training stops at its
26
+ /// next check point, still writes the partially trained model, and returns
27
+ /// its metrics. Because training releases the GVL, another Ruby thread can
28
+ /// cancel a run that is already going.
29
+ #[magnus::wrap(class = "Litsea::CancelToken", free_immediately, size)]
30
+ pub struct CancelToken {
31
+ /// The wrapped token; clones share one flag.
32
+ inner: CoreCancelToken,
33
+ }
34
+
35
+ impl CancelToken {
36
+ /// Creates a token in the "keep running" state.
37
+ ///
38
+ /// # Returns
39
+ /// The new token.
40
+ fn new() -> Self {
41
+ Self {
42
+ inner: CoreCancelToken::new(),
43
+ }
44
+ }
45
+
46
+ /// Requests cancellation.
47
+ fn cancel(&self) {
48
+ self.inner.cancel();
49
+ }
50
+
51
+ /// Returns the token to the "keep running" state.
52
+ fn reset(&self) {
53
+ self.inner.reset();
54
+ }
55
+
56
+ /// Returns whether cancellation has been requested.
57
+ ///
58
+ /// # Returns
59
+ /// `true` once `cancel` has been called.
60
+ fn is_cancelled(&self) -> bool {
61
+ self.inner.is_cancelled()
62
+ }
63
+
64
+ /// Returns the flag a training run should observe.
65
+ ///
66
+ /// # Arguments
67
+ /// * `token` - The caller-supplied token, if any.
68
+ ///
69
+ /// # Returns
70
+ /// A clone of the supplied token, or a fresh one.
71
+ fn resolve(token: Option<&CancelToken>) -> CoreCancelToken {
72
+ token.map_or_else(CoreCancelToken::new, |token| token.inner.clone())
73
+ }
74
+ }
75
+
76
+ /// Reads the optional `cancel:` keyword argument.
77
+ ///
78
+ /// magnus cannot express an optional keyword argument in a `method!` arity,
79
+ /// so the trainers take their arguments through `scan_args`.
80
+ ///
81
+ /// # Arguments
82
+ /// * `args` - The raw Ruby arguments.
83
+ ///
84
+ /// # Returns
85
+ /// The model path and the cancellation flag to observe.
86
+ ///
87
+ /// # Errors
88
+ /// Returns a Ruby `ArgumentError` if the arguments do not match.
89
+ fn scan_train_args(args: &[magnus::Value]) -> Result<(PathBuf, CoreCancelToken), Error> {
90
+ let args = scan_args::<(String,), (), (), (), _, ()>(args)?;
91
+ let (model_path,) = args.required;
92
+ let kwargs = magnus::scan_args::get_kwargs::<_, (), (Option<Option<&CancelToken>>,), ()>(
93
+ args.keywords,
94
+ &[],
95
+ &["cancel"],
96
+ )?;
97
+ let (cancel,) = kwargs.optional;
98
+
99
+ Ok((PathBuf::from(model_path), CancelToken::resolve(cancel.flatten())))
100
+ }
101
+
102
+ /// Extracts training features from a corpus.
103
+ #[magnus::wrap(class = "Litsea::Extractor", free_immediately, size)]
104
+ pub struct Extractor {
105
+ /// The wrapped extractor.
106
+ inner: CoreExtractor,
107
+ }
108
+
109
+ impl Extractor {
110
+ /// Creates an extractor for a language.
111
+ ///
112
+ /// # Arguments
113
+ /// * `language` - A language name or ISO 639-1 code, as a String or Symbol.
114
+ ///
115
+ /// # Returns
116
+ /// The new extractor.
117
+ ///
118
+ /// # Errors
119
+ /// Raises `Litsea::InvalidArgumentError` for an unknown language.
120
+ fn new(language: Value) -> Result<Self, Error> {
121
+ Ok(Self {
122
+ inner: CoreExtractor::new(language_from_value(language)?),
123
+ })
124
+ }
125
+
126
+ /// Extracts boundary-classification features.
127
+ ///
128
+ /// Accepts `tsv:` and `tag_free:` keyword arguments.
129
+ ///
130
+ /// # Arguments
131
+ /// * `args` - `corpus_path`, `features_path`, and the keywords.
132
+ ///
133
+ /// # Returns
134
+ /// `nil`; the features file is written.
135
+ ///
136
+ /// # Errors
137
+ /// Raises `Litsea::IoError` if the corpus cannot be read or the output
138
+ /// cannot be written.
139
+ fn extract(&self, args: &[magnus::Value]) -> Result<(), Error> {
140
+ let args = scan_args::<(String, String), (), (), (), _, ()>(args)?;
141
+ let (corpus_path, features_path) = args.required;
142
+ let kwargs = magnus::scan_args::get_kwargs::<_, (), (Option<bool>, Option<bool>), ()>(
143
+ args.keywords,
144
+ &[],
145
+ &["tsv", "tag_free"],
146
+ )?;
147
+ let (tsv, tag_free) = kwargs.optional;
148
+
149
+ let extracted = without_gvl(|| {
150
+ self.inner.extract(
151
+ std::path::Path::new(&corpus_path),
152
+ std::path::Path::new(&features_path),
153
+ CorpusFormat::from_tsv_flag(tsv.unwrap_or(false)),
154
+ tag_free.unwrap_or(false),
155
+ )
156
+ });
157
+ map_err(extracted)
158
+ }
159
+
160
+ /// Extracts two-stage (segmentation + POS) features.
161
+ ///
162
+ /// Writes `{output_prefix}.stage1`, `.stage2`, and `.lexicon`. Accepts
163
+ /// `feature_set:` and `tsv:` keyword arguments.
164
+ ///
165
+ /// # Arguments
166
+ /// * `args` - `corpus_path`, `output_prefix`, and the keywords.
167
+ ///
168
+ /// # Returns
169
+ /// `nil`; the three files are written.
170
+ ///
171
+ /// # Errors
172
+ /// Raises `Litsea::InvalidArgumentError` for an unknown feature set, or
173
+ /// `Litsea::IoError` on I/O failure.
174
+ fn extract_two_stage(&self, args: &[magnus::Value]) -> Result<(), Error> {
175
+ let args = scan_args::<(String, String), (), (), (), _, ()>(args)?;
176
+ let (corpus_path, output_prefix) = args.required;
177
+ let kwargs = magnus::scan_args::get_kwargs::<_, (), (Option<String>, Option<bool>), ()>(
178
+ args.keywords,
179
+ &[],
180
+ &["feature_set", "tsv"],
181
+ )?;
182
+ let (feature_set, tsv) = kwargs.optional;
183
+
184
+ let feature_set = map_err(parse_feature_set(feature_set.as_deref().unwrap_or("fast")))?;
185
+
186
+ let extracted = without_gvl(|| {
187
+ self.inner.extract_two_stage(
188
+ std::path::Path::new(&corpus_path),
189
+ std::path::Path::new(&output_prefix),
190
+ feature_set,
191
+ CorpusFormat::from_tsv_flag(tsv.unwrap_or(false)),
192
+ )
193
+ });
194
+ map_err(extracted)
195
+ }
196
+ }
197
+
198
+ /// Trains a segmentation model.
199
+ #[magnus::wrap(class = "Litsea::Trainer", free_immediately, size)]
200
+ pub struct Trainer {
201
+ /// The wrapped trainer; `RefCell` because Ruby methods take `&self`.
202
+ inner: RefCell<CoreTrainer>,
203
+ }
204
+
205
+ impl Trainer {
206
+ /// Loads a features file and prepares training.
207
+ ///
208
+ /// # Arguments
209
+ /// * `threshold` - Early-stopping threshold for weak classifiers.
210
+ /// * `num_iterations` - Maximum number of boosting iterations.
211
+ /// * `features_path` - Path to the features file.
212
+ ///
213
+ /// # Returns
214
+ /// The new trainer.
215
+ ///
216
+ /// # Errors
217
+ /// Raises `Litsea::IoError` or `Litsea::ParseError` if the features file
218
+ /// cannot be read.
219
+ fn new(threshold: f64, num_iterations: usize, features_path: String) -> Result<Self, Error> {
220
+ let inner = map_err(CoreTrainer::new(
221
+ threshold,
222
+ num_iterations,
223
+ std::path::Path::new(&features_path),
224
+ ))?;
225
+ Ok(Self {
226
+ inner: RefCell::new(inner),
227
+ })
228
+ }
229
+
230
+ /// Loads an existing model to continue training from it.
231
+ ///
232
+ /// # Arguments
233
+ /// * `model_uri` - Path, `file://` path, or `http(s)://` URL.
234
+ ///
235
+ /// # Returns
236
+ /// `nil`; the model is merged into the learner.
237
+ ///
238
+ /// # Errors
239
+ /// Raises `Litsea::ModelError`, `Litsea::IoError`, or
240
+ /// `Litsea::ParseError`.
241
+ fn load_model(&self, model_uri: String) -> Result<(), Error> {
242
+ let loaded = without_gvl(|| self.inner.borrow_mut().load_model_blocking(&model_uri));
243
+ map_err(loaded)
244
+ }
245
+
246
+ /// Trains the model and writes it to `model_path`.
247
+ ///
248
+ /// The GVL is released while training runs, so another Ruby thread can
249
+ /// cancel it through the `cancel:` token.
250
+ ///
251
+ /// # Arguments
252
+ /// * `args` - `model_path` and an optional `cancel:` token.
253
+ ///
254
+ /// # Returns
255
+ /// The training metrics.
256
+ ///
257
+ /// # Errors
258
+ /// Raises `Litsea::IoError` if the model cannot be written.
259
+ fn train(&self, args: &[magnus::Value]) -> Result<RbBinaryMetrics, Error> {
260
+ let (model_path, cancel) = scan_train_args(args)?;
261
+ let trained = without_gvl(|| self.inner.borrow_mut().train(&cancel, &model_path));
262
+ Ok(RbBinaryMetrics::from(map_err(trained)?))
263
+ }
264
+ }
265
+
266
+ /// Trains a label-agnostic Averaged Perceptron model.
267
+ #[magnus::wrap(class = "Litsea::PerceptronTrainer", free_immediately, size)]
268
+ pub struct PerceptronTrainer {
269
+ /// The wrapped trainer.
270
+ inner: RefCell<CorePerceptronTrainer>,
271
+ }
272
+
273
+ impl PerceptronTrainer {
274
+ /// Loads a features file and prepares training.
275
+ ///
276
+ /// # Arguments
277
+ /// * `num_epochs` - Number of passes over the training data.
278
+ /// * `features_path` - Path to the features file.
279
+ ///
280
+ /// # Returns
281
+ /// The new trainer.
282
+ ///
283
+ /// # Errors
284
+ /// Raises `Litsea::IoError` or `Litsea::ParseError` if the features file
285
+ /// cannot be read.
286
+ fn new(num_epochs: usize, features_path: String) -> Result<Self, Error> {
287
+ let inner =
288
+ map_err(CorePerceptronTrainer::new(num_epochs, std::path::Path::new(&features_path)))?;
289
+ Ok(Self {
290
+ inner: RefCell::new(inner),
291
+ })
292
+ }
293
+
294
+ /// Trains the model and writes it to `model_path`.
295
+ ///
296
+ /// # Arguments
297
+ /// * `args` - `model_path` and an optional `cancel:` token.
298
+ ///
299
+ /// # Returns
300
+ /// The training metrics.
301
+ ///
302
+ /// # Errors
303
+ /// Raises `Litsea::IoError` if the model cannot be written.
304
+ fn train(&self, args: &[magnus::Value]) -> Result<RbMulticlassMetrics, Error> {
305
+ let (model_path, cancel) = scan_train_args(args)?;
306
+ let trained = without_gvl(|| self.inner.borrow_mut().train(&cancel, &model_path));
307
+ Ok(RbMulticlassMetrics::from(map_err(trained)?))
308
+ }
309
+ }
310
+
311
+ /// Trains a two-stage segmentation + POS model.
312
+ ///
313
+ /// A trainer can only be used once: training collapses stage 1 into an
314
+ /// AdaBoost model, which consumes it. `available?` reports the state, and a
315
+ /// second `train` raises.
316
+ #[magnus::wrap(class = "Litsea::TwoStageTrainer", free_immediately, size)]
317
+ pub struct TwoStageTrainer {
318
+ /// The wrapped trainer.
319
+ inner: RefCell<CoreTwoStageTrainer>,
320
+ }
321
+
322
+ impl TwoStageTrainer {
323
+ /// Loads a two-stage features prefix and prepares training.
324
+ ///
325
+ /// Accepts a `dominance:` keyword argument.
326
+ ///
327
+ /// # Arguments
328
+ /// * `args` - `num_epochs`, `features_prefix`, and the keyword.
329
+ ///
330
+ /// # Returns
331
+ /// The new trainer.
332
+ ///
333
+ /// # Errors
334
+ /// Raises `Litsea::InvalidArgumentError` if `dominance` is out of range,
335
+ /// or `Litsea::IoError` / `Litsea::ParseError` if the feature files
336
+ /// cannot be read.
337
+ fn new(args: &[magnus::Value]) -> Result<Self, Error> {
338
+ let args = scan_args::<(usize, String), (), (), (), _, ()>(args)?;
339
+ let (num_epochs, features_prefix) = args.required;
340
+ let kwargs = magnus::scan_args::get_kwargs::<_, (), (Option<f64>,), ()>(
341
+ args.keywords,
342
+ &[],
343
+ &["dominance"],
344
+ )?;
345
+ let (dominance,) = kwargs.optional;
346
+
347
+ let inner = map_err(CoreTwoStageTrainer::new(
348
+ num_epochs,
349
+ dominance.unwrap_or(0.99),
350
+ std::path::Path::new(&features_prefix),
351
+ ))?;
352
+ Ok(Self {
353
+ inner: RefCell::new(inner),
354
+ })
355
+ }
356
+
357
+ /// Returns whether this trainer can still be used.
358
+ ///
359
+ /// # Returns
360
+ /// `false` once `train` has run.
361
+ fn is_available(&self) -> bool {
362
+ self.inner.borrow().is_available()
363
+ }
364
+
365
+ /// Trains both stages and writes the model.
366
+ ///
367
+ /// # Arguments
368
+ /// * `args` - `model_path` and an optional `cancel:` token.
369
+ ///
370
+ /// # Returns
371
+ /// The metrics of both stages.
372
+ ///
373
+ /// # Errors
374
+ /// Raises `Litsea::InvalidArgumentError` if the trainer has already been
375
+ /// used, or `Litsea::IoError` if the model cannot be written.
376
+ fn train(&self, args: &[magnus::Value]) -> Result<RbTwoStageMetrics, Error> {
377
+ let (model_path, cancel) = scan_train_args(args)?;
378
+ let trained = without_gvl(|| self.inner.borrow_mut().train(&cancel, &model_path));
379
+ Ok(RbTwoStageMetrics::from(map_err(trained)?))
380
+ }
381
+ }
382
+
383
+ /// Defines the training classes.
384
+ ///
385
+ /// # Arguments
386
+ /// * `ruby` - The Ruby handle for the current thread.
387
+ /// * `module` - The `Litsea` module to define the classes on.
388
+ ///
389
+ /// # Returns
390
+ /// `()` on success.
391
+ ///
392
+ /// # Errors
393
+ /// Returns a Ruby exception if a class cannot be defined.
394
+ pub fn define(ruby: &Ruby, module: &RModule) -> Result<(), Error> {
395
+ let cancel = module.define_class("CancelToken", ruby.class_object())?;
396
+ cancel.define_singleton_method("new", function!(CancelToken::new, 0))?;
397
+ cancel.define_method("cancel", method!(CancelToken::cancel, 0))?;
398
+ cancel.define_method("reset", method!(CancelToken::reset, 0))?;
399
+ cancel.define_method("cancelled?", method!(CancelToken::is_cancelled, 0))?;
400
+
401
+ let extractor = module.define_class("Extractor", ruby.class_object())?;
402
+ extractor.define_singleton_method("new", function!(Extractor::new, 1))?;
403
+ extractor.define_method("extract", method!(Extractor::extract, -1))?;
404
+ extractor.define_method("extract_two_stage", method!(Extractor::extract_two_stage, -1))?;
405
+
406
+ let trainer = module.define_class("Trainer", ruby.class_object())?;
407
+ trainer.define_singleton_method("new", function!(Trainer::new, 3))?;
408
+ trainer.define_method("load_model", method!(Trainer::load_model, 1))?;
409
+ trainer.define_method("train", method!(Trainer::train, -1))?;
410
+
411
+ let perceptron = module.define_class("PerceptronTrainer", ruby.class_object())?;
412
+ perceptron.define_singleton_method("new", function!(PerceptronTrainer::new, 2))?;
413
+ perceptron.define_method("train", method!(PerceptronTrainer::train, -1))?;
414
+
415
+ let two_stage = module.define_class("TwoStageTrainer", ruby.class_object())?;
416
+ two_stage.define_singleton_method("new", function!(TwoStageTrainer::new, -1))?;
417
+ two_stage.define_method("available?", method!(TwoStageTrainer::is_available, 0))?;
418
+ two_stage.define_method("train", method!(TwoStageTrainer::train, -1))?;
419
+
420
+ Ok(())
421
+ }
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: litsea
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.13.0
5
+ platform: ruby
6
+ authors:
7
+ - Minoru Osuka
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rb_sys
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.9'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.9'
26
+ description: 'Ruby binding for Litsea: word segmentation and Universal POS tagging
27
+ for Japanese, Chinese, Korean, and English. Models are not bundled.'
28
+ executables: []
29
+ extensions:
30
+ - extconf.rb
31
+ extra_rdoc_files: []
32
+ files:
33
+ - Cargo.toml
34
+ - README.md
35
+ - README_ja.md
36
+ - extconf.rb
37
+ - lib/litsea.rb
38
+ - lib/litsea/version.rb
39
+ - src/error.rs
40
+ - src/gvl.rs
41
+ - src/language.rs
42
+ - src/lib.rs
43
+ - src/metrics.rs
44
+ - src/segmenter.rs
45
+ - src/token.rs
46
+ - src/trainer.rs
47
+ homepage: https://github.com/mosuka/litsea
48
+ licenses:
49
+ - MIT
50
+ metadata:
51
+ homepage_uri: https://github.com/mosuka/litsea
52
+ source_code_uri: https://github.com/mosuka/litsea
53
+ bug_tracker_uri: https://github.com/mosuka/litsea/issues
54
+ rubygems_mfa_required: 'true'
55
+ rdoc_options: []
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '3.1'
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ requirements: []
69
+ rubygems_version: 3.6.9
70
+ specification_version: 4
71
+ summary: Ruby binding for Litsea, a compact word segmentation and POS tagging library
72
+ test_files: []