lingua_rs 0.4.5 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 953c59791dc0b1234e1264f3e75881319edfd752a537a3bb0fd02c9830cb63ea
4
- data.tar.gz: 6cf3ce511f0026deb1090c531771a843bb15e0fc4c7637e974f1d50b65330106
3
+ metadata.gz: 5c97528f5d90b92214436ba9938f56a4ffde0d9997081b896d80eed180ebe59a
4
+ data.tar.gz: 147e2bca0a928e18b23027f323ac2a297ccbb5c3480e178cc7054f3c1fc230d1
5
5
  SHA512:
6
- metadata.gz: 067a63ee83c014a358fa48f0df452c626123d8039862cb9dac57b4e733fb379fc6af97102613bd8c664b3762105123223a488b962896bfc419195d237ff6135f
7
- data.tar.gz: 4336e0b72d1dff76ddf28d2721025b4a36a4f6dd031252a09bdfa1eb10fdfc6982dcbc1bbd01d223a84e0f5256c06a98190c4c47a0f154ba06696e4a60abfce7
6
+ metadata.gz: f29859f59b455c0672c8411ab269c78e95b501a350a29dfc5d0966204e593b26aa36268f8c895aeb066962bf7d1d15a75b6ca3bbf576d3074b3b3d34bc393e03
7
+ data.tar.gz: e5320aba0f339e3d6e8ac92a1fff314a0946dd5713fe5e335f057601a97159ffa031b761eace762859180b786670370d94281903ac1c9d96b60e970e959a31ba
data/README.md CHANGED
@@ -25,7 +25,7 @@ Then run `bundle install`.
25
25
 
26
26
  ### Build requirements
27
27
 
28
- A Rust toolchain is required to compile the native extension.
28
+ A Rust toolchain (**1.87+**) is required to compile the native extension.
29
29
 
30
30
  ## Usage
31
31
 
@@ -66,8 +66,6 @@ Lingua.detect('Bonjour', languages: %w[en french deu])
66
66
 
67
67
  ### Options
68
68
 
69
- All options accept both symbol and string keys.
70
-
71
69
  | Option | Type | Description |
72
70
  |---|---|---|
73
71
  | `languages` | `Array<String>` | Restrict detection to these languages (names, ISO 639-1 or ISO 639-3 codes) |
@@ -132,7 +130,61 @@ detector = Lingua::Detector.new(languages: %w[en fr de])
132
130
  detector.detect_multiple(text)
133
131
  ```
134
132
 
135
- ### `Lingua::Language` methods
133
+ ### Parallel batch detection
134
+
135
+ `Lingua::Detector` provides batch methods that process multiple texts in parallel using [Rayon](https://github.com/rayon-rs/rayon). These are significantly faster than calling single-text methods in a loop.
136
+
137
+ ```ruby
138
+ detector = Lingua::Detector.new(languages: %w[en fr de])
139
+
140
+ # Detect language of multiple texts
141
+ detector.detect_batch(['Bonjour le monde', 'Hello world', 'Hallo Welt'])
142
+ # => [#<Lingua::Language French>, #<Lingua::Language English>, #<Lingua::Language German>]
143
+
144
+ # Confidence for a specific language across multiple texts
145
+ detector.confidence_batch(['Bonjour le monde', 'Hello world'], :fr)
146
+ # => [0.8217, 0.0215]
147
+
148
+ # Confidence values for multiple texts
149
+ detector.confidence_values_batch(['Bonjour le monde', 'Hello world'])
150
+ # => [[#<Lingua::ConfidenceResult French (0.82)>, ...], [#<Lingua::ConfidenceResult English (0.91)>, ...]]
151
+
152
+ # Mixed-language detection on multiple texts
153
+ detector.detect_multiple_batch(['Bonjour le monde. Hello world.', 'Ich bin müde. Salut tout le monde.'])
154
+ # => [[#<Lingua::Segment French ...>, #<Lingua::Segment English ...>], [#<Lingua::Segment German ...>, #<Lingua::Segment French ...>]]
155
+ ```
156
+
157
+ | Method | Return type | Description |
158
+ |---|---|---|
159
+ | `detect_batch(texts)` | `Array<Language\|nil>` | Detect language of each text |
160
+ | `confidence_batch(texts, language)` | `Array<Float>` | Confidence for a specific language across texts |
161
+ | `confidence_values_batch(texts)` | `Array<Array<ConfidenceResult>>` | Confidence values for each text |
162
+ | `detect_multiple_batch(texts)` | `Array<Array<Segment>>` | Mixed-language detection for each text |
163
+
164
+ Results are returned in the same order as the input, so you can pair them with `zip`:
165
+
166
+ ```ruby
167
+ texts = ['Bonjour le monde', 'Hello world', 'Hallo Welt']
168
+ texts.zip(detector.detect_batch(texts)) do |text, language|
169
+ puts "#{text} => #{language}"
170
+ end
171
+ ```
172
+
173
+ ### Error handling
174
+
175
+ `Lingua::UnknownLanguageError` (subclass of `ArgumentError`) is raised when an unrecognized language name or code is passed:
176
+
177
+ ```ruby
178
+ begin
179
+ Lingua.detect('Hello', languages: %w[en zzzz])
180
+ rescue Lingua::UnknownLanguageError => e
181
+ puts e.message # => unknown language: "zzzz"
182
+ end
183
+ ```
184
+
185
+ ## API Reference
186
+
187
+ ### `Lingua::Language`
136
188
 
137
189
  `Lingua::Language` objects support equality (`==`) and can be used as Hash keys. You can look up a language by name, ISO 639-1 code, or ISO 639-3 code using `[]`:
138
190
 
@@ -163,7 +215,7 @@ Lingua::Language['xxx'] # => nil
163
215
  | `==` | `Boolean` | Compare two languages |
164
216
  | `hash` | `Integer` | Hash value (usable as Hash key) |
165
217
 
166
- ### `Lingua::ConfidenceResult` methods
218
+ ### `Lingua::ConfidenceResult`
167
219
 
168
220
  Returned by `confidence_values`.
169
221
 
@@ -174,7 +226,7 @@ Returned by `confidence_values`.
174
226
  | `to_s` | `String` | `'French (0.82)'` |
175
227
  | `inspect` | `String` | `'#<Lingua::ConfidenceResult French (0.8217)>'` |
176
228
 
177
- ### `Lingua::Segment` methods
229
+ ### `Lingua::Segment`
178
230
 
179
231
  Returned by `detect_multiple`.
180
232
 
@@ -188,18 +240,49 @@ Returned by `detect_multiple`.
188
240
  | `to_s` | `String` | `'French (0-22): Parlez-vous français? '` |
189
241
  | `inspect` | `String` | `'#<Lingua::Segment French (0-22) "Parlez-vous français? ">'` |
190
242
 
191
- ### Error handling
243
+ ## Command-line tool
192
244
 
193
- `Lingua::UnknownLanguageError` (subclass of `ArgumentError`) is raised when an unrecognized language name or code is passed:
245
+ The gem ships with a `lingua` executable that wraps the library for shell use.
194
246
 
195
- ```ruby
196
- begin
197
- Lingua.detect('Hello', languages: %w[en zzzz])
198
- rescue Lingua::UnknownLanguageError => e
199
- puts e.message # => unknown language: "zzzz"
200
- end
247
+ ```bash
248
+ # Text as argument or on stdin
249
+ lingua 'Bonjour le monde' # => French
250
+ echo 'Hello world' | lingua # => English
251
+
252
+ # Output format
253
+ lingua -f iso 'Bonjour le monde' # => fr
254
+ lingua -f iso6393 'Bonjour le monde' # => fra
255
+ lingua -f json 'Bonjour le monde' # => {"name":"French","iso":"fr","iso6393":"fra"}
256
+
257
+ # Confidence values
258
+ lingua -c -l en,fr,de 'Bonjour le monde'
259
+ # French: 0.8217
260
+ # German: 0.1216
261
+ # English: 0.0567
262
+
263
+ # Mixed-language detection
264
+ lingua -m -l en,fr,de 'Parlez-vous français? Hello world.'
265
+ # French (0-22): Parlez-vous français?
266
+ # English (22-34): Hello world.
267
+
268
+ # Restrict candidates
269
+ lingua -l en,fr,de 'Bonjour'
270
+
271
+ # Batch: one text per line (use "-" for stdin)
272
+ lingua --batch texts.txt
273
+ cat texts.txt | lingua --batch -
201
274
  ```
202
275
 
276
+ | Option | Description |
277
+ |---|---|
278
+ | `-f`, `--format FORMAT` | Output format: `name` (default), `iso`, `iso6393`, `json` |
279
+ | `-c`, `--confidences` | Output confidence for all candidate languages |
280
+ | `-m`, `--multiple` | Detect multiple languages within the text |
281
+ | `-l`, `--languages LIST` | Comma-separated list of languages (names or ISO codes) |
282
+ | `--batch FILE` | Treat each line of FILE (or `-` for stdin) as a separate input |
283
+ | `-v`, `--version` | Show version |
284
+ | `-h`, `--help` | Show help |
285
+
203
286
  ## Optimization: selecting languages
204
287
 
205
288
  By default, all 75 languages are compiled into the native extension (~278 MB). If you only need a subset, select languages at build time to reduce binary size and improve detection speed.
@@ -266,6 +349,8 @@ Supported languages (from [Lingua Rust's official list](https://github.com/pemis
266
349
 
267
350
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake compile` to build the native extension and `rake test` to run the tests.
268
351
 
352
+ To run benchmarks (sequential vs batch performance comparison): `rake bench`
353
+
269
354
  ## Acknowledgements
270
355
 
271
356
  This gem is built on top of [Lingua](https://github.com/pemistahl/lingua-rs) by [Peter M. Stahl](https://github.com/pemistahl), a highly accurate natural language detection library written in Rust.
data/Rakefile CHANGED
@@ -19,4 +19,10 @@ RbSys::ExtensionTask.new('lingua', GEMSPEC) do |ext|
19
19
  ext.lib_dir = 'lib/lingua'
20
20
  end
21
21
 
22
+ desc 'Run benchmarks (sequential vs batch)'
23
+ task bench: :compile do
24
+ ENV['BENCH'] = '1'
25
+ ruby '-Itest', 'test/test_benchmark.rb'
26
+ end
27
+
22
28
  task default: %i[compile test rubocop]
data/exe/lingua ADDED
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'optparse'
5
+ require 'json'
6
+ require 'lingua'
7
+
8
+ def build_parser(options)
9
+ OptionParser.new do |opts|
10
+ opts.banner = 'Usage: lingua [options] [TEXT]'
11
+ opts.separator ''
12
+ opts.separator 'If TEXT is omitted, input is read from stdin.'
13
+ opts.separator ''
14
+ opts.separator 'Options:'
15
+
16
+ opts.on('-f', '--format FORMAT', %w[iso iso6393 name json],
17
+ 'Output format: name (default), iso, iso6393, json') { |f| options[:format] = f.to_sym }
18
+ opts.on('-c', '--confidences',
19
+ 'Output confidence values for all candidate languages') { options[:mode] = :confidences }
20
+ opts.on('-m', '--multiple',
21
+ 'Detect multiple languages within the text') { options[:mode] = :multiple }
22
+ opts.on('-l', '--languages LIST',
23
+ 'Restrict detection to comma-separated languages (names or ISO codes)') do |list|
24
+ options[:languages] = list.split(',').map(&:strip)
25
+ end
26
+ opts.on('--batch FILE',
27
+ 'Treat each line of FILE (or "-" for stdin) as a separate input') { |f| options[:batch] = f }
28
+ opts.on('-h', '--help', 'Show this help') do
29
+ puts opts
30
+ exit
31
+ end
32
+ opts.on('-v', '--version', 'Show version') do
33
+ puts Lingua::VERSION
34
+ exit
35
+ end
36
+ end
37
+ end
38
+
39
+ options = { format: :name, mode: :detect, languages: nil, batch: nil }
40
+ build_parser(options).parse!
41
+
42
+ def format_language(lang, format)
43
+ return nil if lang.nil?
44
+
45
+ case format
46
+ when :iso then lang.to_iso
47
+ when :iso6393 then lang.to_iso6393
48
+ when :name then lang.name
49
+ when :json then { name: lang.name, iso: lang.to_iso, iso6393: lang.to_iso6393 }
50
+ end
51
+ end
52
+
53
+ def render_detect(lang, format)
54
+ formatted = format_language(lang, format)
55
+ if format == :json
56
+ JSON.generate(formatted)
57
+ else
58
+ formatted.to_s
59
+ end
60
+ end
61
+
62
+ def render_confidences(results, format)
63
+ if format == :json
64
+ JSON.generate(results.map do |r|
65
+ { language: format_language(r.language, :iso), confidence: r.confidence }
66
+ end)
67
+ else
68
+ results.map { |r| "#{format_language(r.language, format)}: #{format('%.4f', r.confidence)}" }
69
+ .join("\n")
70
+ end
71
+ end
72
+
73
+ def render_multiple(segments, format)
74
+ if format == :json
75
+ JSON.generate(segments.map do |s|
76
+ { language: format_language(s.language, :iso),
77
+ start_index: s.start_index,
78
+ end_index: s.end_index,
79
+ text: s.text }
80
+ end)
81
+ else
82
+ segments.map do |s|
83
+ "#{format_language(s.language, format)} (#{s.start_index}-#{s.end_index}): #{s.text}"
84
+ end.join("\n")
85
+ end
86
+ end
87
+
88
+ def process(text, detector, options)
89
+ case options[:mode]
90
+ when :detect
91
+ render_detect(detector.detect(text), options[:format])
92
+ when :confidences
93
+ render_confidences(detector.confidence_values(text), options[:format])
94
+ when :multiple
95
+ render_multiple(detector.detect_multiple(text), options[:format])
96
+ end
97
+ end
98
+
99
+ begin
100
+ detector_options = {}
101
+ detector_options[:languages] = options[:languages] if options[:languages]
102
+ detector = Lingua::Detector.new(**detector_options)
103
+
104
+ if options[:batch]
105
+ io = options[:batch] == '-' ? $stdin : File.open(options[:batch], 'r')
106
+ io.each_line do |line|
107
+ line = line.chomp
108
+ next if line.empty?
109
+
110
+ puts process(line, detector, options)
111
+ end
112
+ io.close unless io == $stdin
113
+ else
114
+ text = ARGV.empty? ? $stdin.read : ARGV.join(' ')
115
+ text = text.strip
116
+ if text.empty?
117
+ warn 'lingua: no input text'
118
+ exit 1
119
+ end
120
+ puts process(text, detector, options)
121
+ end
122
+ rescue Lingua::UnknownLanguageError => e
123
+ warn "lingua: #{e.message}"
124
+ exit 1
125
+ end
@@ -13,6 +13,10 @@ pub fn define(ruby: &Ruby, module: &RModule) -> Result<(), Error> {
13
13
  class.define_method("confidence", method!(RubyDetector::confidence, 2))?;
14
14
  class.define_method("confidence_values", method!(RubyDetector::confidence_values, 1))?;
15
15
  class.define_method("detect_multiple", method!(RubyDetector::detect_multiple, 1))?;
16
+ class.define_method("detect_batch", method!(RubyDetector::detect_batch, 1))?;
17
+ class.define_method("detect_multiple_batch", method!(RubyDetector::detect_multiple_batch, 1))?;
18
+ class.define_method("confidence_values_batch", method!(RubyDetector::confidence_values_batch, 1))?;
19
+ class.define_method("confidence_batch", method!(RubyDetector::confidence_batch, 2))?;
16
20
  Ok(())
17
21
  }
18
22
 
@@ -81,6 +85,73 @@ impl RubyDetector {
81
85
  pub fn detect_multiple(&self, subject: String) -> Result<RArray, Error> {
82
86
  compute_detect_multiple(&self.detector, &subject)
83
87
  }
88
+
89
+ pub fn detect_batch(&self, texts: Vec<String>) -> Result<RArray, Error> {
90
+ let ruby = Ruby::get().unwrap();
91
+ let results = self.detector.detect_languages_in_parallel_of(&texts);
92
+ let array = ruby.ary_new_capa(results.len());
93
+ for result in results {
94
+ array.push(result.map(WrappedLanguage))?;
95
+ }
96
+ Ok(array)
97
+ }
98
+
99
+ pub fn detect_multiple_batch(&self, texts: Vec<String>) -> Result<RArray, Error> {
100
+ let ruby = Ruby::get().unwrap();
101
+ let results = self.detector.detect_multiple_languages_in_parallel_of(&texts);
102
+ let outer = ruby.ary_new_capa(results.len());
103
+ for (detection_results, subject) in results.iter().zip(texts.iter()) {
104
+ let inner = ruby.ary_new_capa(detection_results.len());
105
+ for r in detection_results {
106
+ let text = subject[r.start_index()..r.end_index()].to_string();
107
+ let start_index = subject[..r.start_index()].chars().count();
108
+ let end_index = start_index + text.chars().count();
109
+ inner.push(Segment {
110
+ language: r.language(),
111
+ start_index,
112
+ end_index,
113
+ word_count: r.word_count(),
114
+ text,
115
+ })?;
116
+ }
117
+ outer.push(inner)?;
118
+ }
119
+ Ok(outer)
120
+ }
121
+
122
+ pub fn confidence_values_batch(&self, texts: Vec<String>) -> Result<RArray, Error> {
123
+ let ruby = Ruby::get().unwrap();
124
+ let results = self.detector.compute_language_confidence_values_in_parallel(&texts);
125
+ let outer = ruby.ary_new_capa(results.len());
126
+ for values in results {
127
+ let inner = ruby.ary_new_capa(values.len());
128
+ for (language, confidence) in values {
129
+ inner.push(ConfidenceResult {
130
+ language,
131
+ confidence,
132
+ })?;
133
+ }
134
+ outer.push(inner)?;
135
+ }
136
+ Ok(outer)
137
+ }
138
+
139
+ pub fn confidence_batch(&self, texts: Vec<String>, language: magnus::Value) -> Result<RArray, Error> {
140
+ let ruby = Ruby::get().unwrap();
141
+ let language_str = value_to_string(language)?;
142
+ let lang = parse_language(&language_str).ok_or_else(|| {
143
+ Error::new(
144
+ unknown_language_error(&ruby),
145
+ format!("unknown language: \"{language_str}\""),
146
+ )
147
+ })?;
148
+ let results = self.detector.compute_language_confidence_in_parallel(&texts, lang);
149
+ let array = ruby.ary_new_capa(results.len());
150
+ for score in results {
151
+ array.push(score)?;
152
+ }
153
+ Ok(array)
154
+ }
84
155
  }
85
156
 
86
157
  pub fn compute_detect_multiple(
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lingua
4
- VERSION = '0.4.5'
4
+ VERSION = '0.6.0'
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lingua_rs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.5
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sébastien Vrillaud
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-04-08 00:00:00.000000000 Z
10
+ date: 2026-04-20 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rb_sys
@@ -42,7 +42,8 @@ description: Native Ruby bindings for the Lingua Rust library, bringing highly a
42
42
  scores and ISO 639-1/639-3 language codes.
43
43
  email:
44
44
  - kochka@gmail.com
45
- executables: []
45
+ executables:
46
+ - lingua
46
47
  extensions:
47
48
  - ext/lingua/extconf.rb
48
49
  extra_rdoc_files: []
@@ -50,6 +51,7 @@ files:
50
51
  - LICENSE.txt
51
52
  - README.md
52
53
  - Rakefile
54
+ - exe/lingua
53
55
  - ext/lingua/Cargo.toml
54
56
  - ext/lingua/extconf.rb
55
57
  - ext/lingua/src/confidence_result.rs
@@ -84,7 +86,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
84
86
  - !ruby/object:Gem::Version
85
87
  version: '0'
86
88
  requirements: []
87
- rubygems_version: 4.0.9
89
+ rubygems_version: 4.0.10
88
90
  specification_version: 4
89
91
  summary: High-accuracy language detection powered by Lingua (Rust).
90
92
  test_files: []