psychowl 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 +21 -0
- data/LICENSE.txt +21 -0
- data/README.md +212 -0
- data/exe/psychowl +106 -0
- data/ext/psychowl_native/core/data/LICENSE-whatlang +26 -0
- data/ext/psychowl_native/core/data/README.md +32 -0
- data/ext/psychowl_native/core/data/alphabets.tsv +16 -0
- data/ext/psychowl_native/core/data/languages.tsv +20 -0
- data/ext/psychowl_native/core/data/scripts.tsv +28 -0
- data/ext/psychowl_native/core/data/trigrams/ara.txt +302 -0
- data/ext/psychowl_native/core/data/trigrams/deu.txt +302 -0
- data/ext/psychowl_native/core/data/trigrams/eng.txt +302 -0
- data/ext/psychowl_native/core/data/trigrams/fra.txt +302 -0
- data/ext/psychowl_native/core/data/trigrams/hin.txt +302 -0
- data/ext/psychowl_native/core/data/trigrams/ind.txt +302 -0
- data/ext/psychowl_native/core/data/trigrams/ita.txt +302 -0
- data/ext/psychowl_native/core/data/trigrams/nld.txt +302 -0
- data/ext/psychowl_native/core/data/trigrams/pol.txt +302 -0
- data/ext/psychowl_native/core/data/trigrams/por.txt +302 -0
- data/ext/psychowl_native/core/data/trigrams/rus.txt +302 -0
- data/ext/psychowl_native/core/data/trigrams/spa.txt +302 -0
- data/ext/psychowl_native/core/data/trigrams/tur.txt +302 -0
- data/ext/psychowl_native/core/data/trigrams/ukr.txt +302 -0
- data/ext/psychowl_native/core/data/trigrams/vie.txt +302 -0
- data/lib/psychowl/active_model.rb +64 -0
- data/lib/psychowl/data_file.rb +14 -0
- data/lib/psychowl/detector.rb +123 -0
- data/lib/psychowl/engine.rb +23 -0
- data/lib/psychowl/info.rb +39 -0
- data/lib/psychowl/lang.rb +55 -0
- data/lib/psychowl/locale/en.yml +6 -0
- data/lib/psychowl/lookup.rb +25 -0
- data/lib/psychowl/native_speedup.rb +46 -0
- data/lib/psychowl/postgres.rb +35 -0
- data/lib/psychowl/railtie.rb +10 -0
- data/lib/psychowl/ruby_engine/segmentation.rb +84 -0
- data/lib/psychowl/ruby_engine/trigrams.rb +74 -0
- data/lib/psychowl/ruby_engine.rb +166 -0
- data/lib/psychowl/script.rb +39 -0
- data/lib/psychowl/segment.rb +24 -0
- data/lib/psychowl/tables.rb +55 -0
- data/lib/psychowl/text.rb +26 -0
- data/lib/psychowl/version.rb +5 -0
- data/lib/psychowl.rb +82 -0
- metadata +91 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d819bc7757a98dbd6a87c9716fc74433c4c28709b5a4262a0211e407a7c8c68b
|
|
4
|
+
data.tar.gz: 04fd01219832809efce54feed609c28977ba4c2991f100c7b7983daaceaf6763
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 2654f694df5a85b1ad9f5731bcbde84e505bbdfc24ac6bb9c4b253e437b76aa06a8ff9f651d0bf1e3c86206241454de34e8bd2efc7299fc49a00d27402dd5947
|
|
7
|
+
data.tar.gz: 400a6c781067e429ab9c619f184b4750b6882d38703258ec53fc10bb86b440694fc7e4058591e76b34faf6c09538cd7251ba697013678c3fc9e922014c81396d
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-10-08)
|
|
4
|
+
|
|
5
|
+
First release, for Ruby 4.0+ (CRuby, JRuby 10.1+, TruffleRuby 40+): 18 languages
|
|
6
|
+
and 25 scripts. Wave 1: Arabic, English, French, German, Italian, Japanese,
|
|
7
|
+
Mandarin, Portuguese, Russian, Spanish. Wave 2: Dutch, Hindi, Indonesian,
|
|
8
|
+
Korean, Polish, Turkish, Ukrainian, Vietnamese.
|
|
9
|
+
|
|
10
|
+
- Pure Ruby engine plus the `psychowl` Rust crate, loaded as an optional
|
|
11
|
+
native extension; a parity suite keeps both bit-identical.
|
|
12
|
+
- Precompiled gems for Linux (x86_64/aarch64, glibc and musl), macOS
|
|
13
|
+
(arm64/x86_64) and FreeBSD 15.1 (amd64); a pure Ruby gem for everything
|
|
14
|
+
else.
|
|
15
|
+
- `detect`, `detect_lang`, `detect_script`, `candidates`, `segments`,
|
|
16
|
+
`detect_many`, `scripts`; allowlists and denylists that also apply to
|
|
17
|
+
single-language scripts and Han.
|
|
18
|
+
- `Lang` with ISO 639-3/639-1 codes, `#locale` and `#pg_regconfig`.
|
|
19
|
+
- ActiveModel `language:` validator, `psychowl` CLI.
|
|
20
|
+
- Language tables as plain text, a whatlang importer and a trigram profile
|
|
21
|
+
trainer.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Abdelkader Boudih
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# psychowl
|
|
2
|
+
|
|
3
|
+
It knows what language you speak. It knows you skipped your lesson.
|
|
4
|
+
|
|
5
|
+
Language and script detection for Ruby and Rust. psychowl follows the
|
|
6
|
+
[Matryoshka](https://github.com/seuros/matryoshka) FFI Hybrid pattern:
|
|
7
|
+
|
|
8
|
+
- a **pure Ruby engine** that runs everywhere (CRuby, JRuby 10.1+, TruffleRuby 40+),
|
|
9
|
+
- the same algorithm in **Rust** (the `psychowl` crate), loaded as a native
|
|
10
|
+
extension when available, 20 to 500 times faster,
|
|
11
|
+
- one set of **plain-text language tables** both engines read, and a parity
|
|
12
|
+
test suite holding them to bit-identical results.
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
require "psychowl"
|
|
16
|
+
|
|
17
|
+
info = Psychowl.detect("El presidente dijo que la economía va muy bien. Los precios todavía no se han enterado.")
|
|
18
|
+
info.lang.code # => "spa"
|
|
19
|
+
info.lang.locale # => :es
|
|
20
|
+
info.script.name # => "Latin"
|
|
21
|
+
info.confidence # => 1.0 (more than the economy gets)
|
|
22
|
+
info.reliable? # => true
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## The owl has heard things
|
|
26
|
+
|
|
27
|
+
Every line below is detected correctly, at full confidence, by both engines.
|
|
28
|
+
Heads of state make the best test data: they talk a lot and say nothing.
|
|
29
|
+
|
|
30
|
+
| | Text | Roughly |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| eng | Nobody detects languages better than me, believe me. Tremendous trigrams, the best trigrams, everybody says so. | The best trigrams. Everybody says so. |
|
|
33
|
+
| spa | El presidente dijo que la economía va muy bien. Los precios todavía no se han enterado. | The president said the economy is doing great. Prices haven't heard yet. |
|
|
34
|
+
| fra | Traversez la rue, je vous trouverai un travail, a dit le président avant de repartir en jet privé. | "Cross the street, I'll find you a job," said the president, boarding his private jet. |
|
|
35
|
+
| deu | Die Deutsche Bahn ist heute pünktlich. Wir ermitteln. | Deutsche Bahn is on time today. We are investigating. |
|
|
36
|
+
| ita | Il presidente ha promesso che il ponte sarà finito entro le prossime elezioni. Quali elezioni non lo ha detto. | The bridge will be done by the next election. He didn't say which one. |
|
|
37
|
+
| por | O presidente prometeu que desta vez a obra termina no prazo. A obra começou em mil novecentos e oitenta e dois. | This time the construction finishes on schedule. It started in 1982. |
|
|
38
|
+
| rus | Это не война, это специальная военная операция. | It's not a war, it's a special military operation. |
|
|
39
|
+
| ara | فاز الرئيس في الانتخابات بنسبة تسعة وتسعين في المئة. مرة أخرى. | The president won the election with 99%. Again. |
|
|
40
|
+
| cmn | 小熊维尼今天又被禁止了。 | Winnie the Pooh was banned again today. |
|
|
41
|
+
| jpn | 首相は「前向きに検討します」と言いました。 | The prime minister said he will "consider it positively". |
|
|
42
|
+
| kor | 대통령은 부동산 가격이 안정되었다고 말했다. 집값은 그 말을 듣지 못했다. | The president said housing prices have stabilized. Housing prices didn't hear him. |
|
|
43
|
+
| hin | नेताजी ने वादा किया कि इस बार बिजली नहीं जाएगी। फिर बिजली चली गई। | The politician promised the power won't go out this time. Then the power went out. |
|
|
44
|
+
| tur | Bakan, ekonominin uçuşa geçtiğini söyledi. Kimse paraşüt dağıtmadı. | The minister said the economy has taken off. Nobody handed out parachutes. |
|
|
45
|
+
| nld | De regering is gevallen. Dat is al de derde keer deze maand, en het is pas dinsdag. | The government has fallen. Third time this month, and it's only Tuesday. |
|
|
46
|
+
| pol | Prezydent obiecał, że pociąg przyjedzie punktualnie. Pociąg nie został o tym poinformowany. | The president promised the train will be on time. Nobody told the train. |
|
|
47
|
+
| ukr | Сова нагадує: урок не пройдено. Вона вже їде до тебе. | The owl reminds you: lesson not done. It is already on its way to you. |
|
|
48
|
+
| ind | Pejabat itu bilang jalan tol akan selesai tahun depan. Dia mengatakan hal yang sama sepuluh tahun lalu. | The official says the toll road is done next year. He said the same ten years ago. |
|
|
49
|
+
| vie | Ông quan chức hứa sẽ sửa đường ngay tuần sau. Tuần sau đó đã kéo dài ba năm. | The official promised to fix the road next week. That week has lasted three years. |
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
Requires Ruby 4.0 or newer.
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
gem "psychowl"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Precompiled native gems ship for Linux (x86_64 and aarch64; glibc 2.34+ such
|
|
60
|
+
as Debian 13, Ubuntu 26.04, RHEL 10, Fedora, Arch or Omarchy, plus musl for
|
|
61
|
+
Alpine), macOS 13+ (arm64, x86_64) and FreeBSD 15.1 (amd64). No Rust, no
|
|
62
|
+
compiler, nothing to build.
|
|
63
|
+
Windows, JRuby and TruffleRuby get the pure Ruby gem: same results, just
|
|
64
|
+
slower. Where a shipped native extension can't load (glibc older than 2.34,
|
|
65
|
+
Alpine without `libgcc`), psychowl warns and uses its Ruby engine.
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
Psychowl.backend # => :native or :ruby
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Set `DISABLE_PSYCHOWL_NATIVE=1` (or `DISABLE_MATRYOSHKA_NATIVE=1`) to force
|
|
72
|
+
the Ruby engine.
|
|
73
|
+
|
|
74
|
+
## Usage
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
Psychowl.detect_lang("Die Deutsche Bahn ist heute pünktlich. Wir ermitteln.")
|
|
78
|
+
# => #<Psychowl::Lang deu (German)>
|
|
79
|
+
|
|
80
|
+
Psychowl.detect_script("Это не война, это специальная военная операция.")
|
|
81
|
+
# => #<Psychowl::Script Cyrillic> (the owl calls it what it is)
|
|
82
|
+
|
|
83
|
+
promise = "O presidente prometeu que desta vez a obra termina no prazo."
|
|
84
|
+
|
|
85
|
+
# Restrict the candidates (ISO 639-3 or 639-1 codes, or Lang objects)
|
|
86
|
+
Psychowl.detect_lang(promise, allowlist: %i[en es]) # => #<Psychowl::Lang spa (Spanish)>
|
|
87
|
+
Psychowl.detect_lang(promise, denylist: [:por]) # => #<Psychowl::Lang spa (Spanish)>
|
|
88
|
+
|
|
89
|
+
# Reuse a configured detector (frozen, thread-safe, unlike the coalition)
|
|
90
|
+
detector = Psychowl::Detector.new(allowlist: %i[en fr de])
|
|
91
|
+
detector.detect_lang("Traversez la rue, je vous trouverai un travail, a dit le président.")
|
|
92
|
+
# => #<Psychowl::Lang fra (French)>
|
|
93
|
+
|
|
94
|
+
# Every candidate with its score, best first
|
|
95
|
+
Psychowl.candidates(promise, limit: 3)
|
|
96
|
+
# => [[#<Psychowl::Lang por (Portuguese)>, 0.71], [#<Psychowl::Lang spa (Spanish)>, 0.67], [#<Psychowl::Lang ita (Italian)>, 0.66]]
|
|
97
|
+
|
|
98
|
+
# Mixed-language text, per sentence, with character ranges (summit transcripts)
|
|
99
|
+
summit = "Nobody detects languages better than me, believe me. Traversez la rue, je vous trouverai un travail."
|
|
100
|
+
Psychowl.segments(summit).map { [_1.lang.code, _1.range] }
|
|
101
|
+
# => [["eng", 0...53], ["fra", 53...100]]
|
|
102
|
+
|
|
103
|
+
# Many texts at once (spread over all cores with the native engine)
|
|
104
|
+
Psychowl.detect_many(press_releases.map(&:body))
|
|
105
|
+
|
|
106
|
+
# Writing systems in a text
|
|
107
|
+
Psychowl.scripts("Error 404: смысл жизни not found")
|
|
108
|
+
# => {#<Psychowl::Script Latin> => 0.565, #<Psychowl::Script Cyrillic> => 0.435}
|
|
109
|
+
|
|
110
|
+
# Pattern matching
|
|
111
|
+
case Psychowl.detect(speech)
|
|
112
|
+
in {lang: {code: "eng"}, reliable: true} then :tremendous
|
|
113
|
+
in {reliable: false} then :fake_news
|
|
114
|
+
in nil then :no_comment
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Invalid input raises: `TypeError` for non-strings, `EncodingError` for bytes
|
|
119
|
+
that are not valid UTF-8 (other encodings are converted, binary strings are
|
|
120
|
+
accepted when they hold valid UTF-8), `ArgumentError` for unsupported
|
|
121
|
+
language codes.
|
|
122
|
+
|
|
123
|
+
### Short texts
|
|
124
|
+
|
|
125
|
+
Detection works on character trigrams, so it needs some text. A couple of
|
|
126
|
+
words is a guess: "Hello" comes out as Italian with a confidence of 0.08,
|
|
127
|
+
and the owl stands by it. Check `reliable?` or `confidence` before trusting a
|
|
128
|
+
result, or restrict the candidates with an allowlist. Same rule as campaign
|
|
129
|
+
promises.
|
|
130
|
+
|
|
131
|
+
## Rails
|
|
132
|
+
|
|
133
|
+
The `language:` validator loads automatically in Rails apps (elsewhere:
|
|
134
|
+
`require "psychowl/active_model"`).
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
class CampaignPromise < ApplicationRecord
|
|
138
|
+
validates :text, language: { in: %i[en es] }
|
|
139
|
+
validates :fine_print, language: { reliable: true }, allow_blank: true
|
|
140
|
+
validates :excuse, language: { minimum_confidence: 0.5 } # "we will consider it positively" is not an excuse
|
|
141
|
+
end
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Error keys are `:language_undetected`, `:language_not_allowed` and
|
|
145
|
+
`:language_uncertain`, each with a `%{language}` interpolation; English
|
|
146
|
+
messages ship with the gem.
|
|
147
|
+
|
|
148
|
+
PostgreSQL full-text search configuration for a language:
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
config = Psychowl.detect(speech.transcript)&.lang&.pg_regconfig || "simple" # => "german"
|
|
152
|
+
Speech.where("to_tsvector(?::regconfig, transcript) @@ plainto_tsquery(?::regconfig, ?)", config, config, "pünktlich")
|
|
153
|
+
# => [] (as expected)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## CLI
|
|
157
|
+
|
|
158
|
+
```console
|
|
159
|
+
$ echo "Nobody detects languages better than me, believe me." | psychowl
|
|
160
|
+
eng English Latin 0.89 unreliable
|
|
161
|
+
$ psychowl --candidates 3 campaign_promises.txt
|
|
162
|
+
$ psychowl --lines --json press_conference.txt # one result per line, batch
|
|
163
|
+
$ psychowl --segments --allow en,fr summit_transcript.txt
|
|
164
|
+
$ psychowl --scripts leaked_memo.txt
|
|
165
|
+
$ psychowl --languages
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Exit status: 0 when a language was found, 1 when not, 2 on bad input.
|
|
169
|
+
|
|
170
|
+
## Supported languages
|
|
171
|
+
|
|
172
|
+
Languages are added in waves; adding one is a data change, not a code change
|
|
173
|
+
(see [`data/README.md`](ext/psychowl_native/core/data/README.md)).
|
|
174
|
+
|
|
175
|
+
| Wave | Languages |
|
|
176
|
+
|---|---|
|
|
177
|
+
| 1 (0.1) | Arabic, English, French, German, Italian, Japanese, Mandarin, Portuguese, Russian, Spanish |
|
|
178
|
+
| 2 (0.1) | Dutch, Hindi, Indonesian, Korean, Polish, Turkish, Ukrainian, Vietnamese |
|
|
179
|
+
| 3 | the rest of whatlang's 70 |
|
|
180
|
+
| 4 | new languages trained from our own corpora |
|
|
181
|
+
|
|
182
|
+
Script detection covers 25 writing systems already. A text in a script with
|
|
183
|
+
no supported language yet (Greek, Thai...) has a script but no language.
|
|
184
|
+
Text in an unsupported language written in a supported script gets the
|
|
185
|
+
closest supported language, usually with a low confidence.
|
|
186
|
+
|
|
187
|
+
## Rust
|
|
188
|
+
|
|
189
|
+
The engine is a standalone crate, see
|
|
190
|
+
[`ext/psychowl_native/core`](ext/psychowl_native/core/README.md).
|
|
191
|
+
|
|
192
|
+
## Development
|
|
193
|
+
|
|
194
|
+
```console
|
|
195
|
+
bundle install
|
|
196
|
+
rake test:all # cargo test, then the suite on both engines (incl. parity)
|
|
197
|
+
rake test:ruby # pure Ruby engine only
|
|
198
|
+
rake lint # cargo fmt --check + clippy
|
|
199
|
+
rake "psychowl:train[eng]" # rebuild a profile from corpus/eng.txt
|
|
200
|
+
ruby bench/engines.rb # Ruby vs Rust timings
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Credits
|
|
204
|
+
|
|
205
|
+
The algorithm and the initial language profiles are ported from
|
|
206
|
+
[whatlang](https://github.com/greyblake/whatlang-rs) by Sergey Potapov and
|
|
207
|
+
contributors (MIT); see
|
|
208
|
+
[`LICENSE-whatlang`](ext/psychowl_native/core/data/LICENSE-whatlang).
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
MIT
|
data/exe/psychowl
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'optparse'
|
|
6
|
+
require 'psychowl'
|
|
7
|
+
|
|
8
|
+
options = { mode: :detect, json: false }
|
|
9
|
+
|
|
10
|
+
parser = OptionParser.new do |opts|
|
|
11
|
+
opts.banner = <<~BANNER
|
|
12
|
+
Usage: psychowl [options] [FILE...]
|
|
13
|
+
|
|
14
|
+
Detects the language of each FILE, or of standard input.
|
|
15
|
+
|
|
16
|
+
BANNER
|
|
17
|
+
opts.on('-j', '--json', 'JSON output') { options[:json] = true }
|
|
18
|
+
opts.on('-l', '--lines', 'Detect every line separately (batch)') { options[:mode] = :lines }
|
|
19
|
+
opts.on('-c', '--candidates [N]', Integer, 'Rank candidate languages (top N)') do |limit|
|
|
20
|
+
options[:mode] = :candidates
|
|
21
|
+
options[:limit] = limit
|
|
22
|
+
end
|
|
23
|
+
opts.on('-s', '--segments', 'Split mixed-language text into segments') { options[:mode] = :segments }
|
|
24
|
+
opts.on('--scripts', 'Show the writing systems used') { options[:mode] = :scripts }
|
|
25
|
+
opts.on('-a', '--allow LANGS', Array, 'Only consider these languages, e.g. en,fr') do |langs|
|
|
26
|
+
options[:allowlist] = langs
|
|
27
|
+
end
|
|
28
|
+
opts.on('-d', '--deny LANGS', Array, 'Never consider these languages') { options[:denylist] = it }
|
|
29
|
+
opts.on('--languages', 'List supported languages') { options[:mode] = :languages }
|
|
30
|
+
opts.on('-v', '--version', 'Show version and engine') do
|
|
31
|
+
puts "psychowl #{Psychowl::VERSION} (#{Psychowl.backend} engine)"
|
|
32
|
+
exit
|
|
33
|
+
end
|
|
34
|
+
opts.on('-h', '--help', 'Show this help') do
|
|
35
|
+
puts opts
|
|
36
|
+
exit
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def info_hash(info)
|
|
41
|
+
return nil unless info
|
|
42
|
+
|
|
43
|
+
{ lang: info.lang.code, iso639_1: info.lang.iso639_1, name: info.lang.eng_name,
|
|
44
|
+
script: info.script.name, confidence: info.confidence, reliable: info.reliable? }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def info_line(info)
|
|
48
|
+
return 'unknown' unless info
|
|
49
|
+
|
|
50
|
+
[info.lang.code, info.lang.eng_name, info.script.name, format('%.2f', info.confidence),
|
|
51
|
+
info.reliable? ? 'reliable' : 'unreliable'].join("\t")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def emit(options, json, text)
|
|
55
|
+
puts options[:json] ? JSON.generate(json) : text
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
begin
|
|
59
|
+
parser.parse!
|
|
60
|
+
filter = options.slice(:allowlist, :denylist)
|
|
61
|
+
detector = Psychowl::Detector.new(**filter)
|
|
62
|
+
|
|
63
|
+
if options[:mode] == :languages
|
|
64
|
+
langs = Psychowl::Lang.all
|
|
65
|
+
emit(options, langs.map { |lang| { code: lang.code, iso639_1: lang.iso639_1, name: lang.eng_name } },
|
|
66
|
+
langs.map { |lang| [lang.code, lang.iso639_1, lang.eng_name, lang.name].join("\t") }.join("\n"))
|
|
67
|
+
exit
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
input = ARGF.read.force_encoding(Encoding::UTF_8)
|
|
71
|
+
detected = true
|
|
72
|
+
|
|
73
|
+
case options[:mode]
|
|
74
|
+
when :detect
|
|
75
|
+
info = detector.detect(input)
|
|
76
|
+
detected = !info.nil?
|
|
77
|
+
emit(options, info_hash(info), info_line(info))
|
|
78
|
+
when :lines
|
|
79
|
+
lines = input.lines(chomp: true)
|
|
80
|
+
infos = detector.detect_many(lines)
|
|
81
|
+
emit(options, infos.map { info_hash(it) }, infos.map { info_line(it) }.join("\n"))
|
|
82
|
+
when :candidates
|
|
83
|
+
ranked = detector.candidates(input, limit: options[:limit])
|
|
84
|
+
detected = !ranked.empty?
|
|
85
|
+
emit(options, ranked.map { |lang, score| { lang: lang.code, name: lang.eng_name, score: } },
|
|
86
|
+
ranked.map { |lang, score| [lang.code, lang.eng_name, format('%.4f', score)].join("\t") }.join("\n"))
|
|
87
|
+
when :segments
|
|
88
|
+
segments = detector.segments(input)
|
|
89
|
+
detected = !segments.empty?
|
|
90
|
+
emit(options,
|
|
91
|
+
segments.map do |segment|
|
|
92
|
+
info_hash(segment.info).merge(start: segment.range.begin, end: segment.range.end, text: segment.text)
|
|
93
|
+
end,
|
|
94
|
+
segments.map { |segment| [segment.range, info_line(segment.info), segment.text.strip].join("\t") }.join("\n"))
|
|
95
|
+
when :scripts
|
|
96
|
+
shares = detector.scripts(input)
|
|
97
|
+
detected = !shares.empty?
|
|
98
|
+
emit(options, shares.to_h { |script, share| [script.name, share] },
|
|
99
|
+
shares.map { |script, share| [script.name, format('%.1f%%', share * 100)].join("\t") }.join("\n"))
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
exit(detected ? 0 : 1)
|
|
103
|
+
rescue OptionParser::ParseError, ArgumentError, EncodingError, SystemCallError => e
|
|
104
|
+
warn "psychowl: #{e.message}"
|
|
105
|
+
exit 2
|
|
106
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
(The MIT License)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017 Sergey Potapov <blake131313@gmail.com>
|
|
4
|
+
Copyright (c) 2014 Titus Wormer <tituswormer@gmail.com>
|
|
5
|
+
Copyright (c) 2008 Kent S Johnson
|
|
6
|
+
Copyright (c) 2006 Jacob R Rideout <kde@jacobrideout.net>
|
|
7
|
+
Copyright (c) 2004 Maciej Ceglowski
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
10
|
+
a copy of this software and associated documentation files (the
|
|
11
|
+
'Software'), to deal in the Software without restriction, including
|
|
12
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
13
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
14
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
15
|
+
the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be
|
|
18
|
+
included in all copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
|
21
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
22
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
23
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
24
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
25
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
26
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# psychowl language data
|
|
2
|
+
|
|
3
|
+
Plain-text tables shared by both engines: the Rust crate compiles them in
|
|
4
|
+
(`build.rs`), the pure Ruby engine reads them at load time. Both must give
|
|
5
|
+
identical results, so these files are the single source of truth.
|
|
6
|
+
|
|
7
|
+
Lines starting with `#` are comments. Fields are tab-separated.
|
|
8
|
+
|
|
9
|
+
| File | Content |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `languages.tsv` | `code`, `iso639_1`, `eng_name`, `name`. Sorted by code; line order is the language index. |
|
|
12
|
+
| `scripts.tsv` | `name`, `languages` (comma-separated codes, `-` for none), `ranges` (hex code points, inclusive, disjoint across scripts). Line order is the script index and the tie-break priority. |
|
|
13
|
+
| `alphabets.tsv` | `code`, `letters`: lowercase letters a language commonly uses. Needed for languages sharing a script. |
|
|
14
|
+
| `trigrams/<code>.txt` | The language's 300 most frequent character trigrams, most frequent first, one per line. `_` stands for a word boundary (space). Needed for languages sharing a script. |
|
|
15
|
+
|
|
16
|
+
## Adding a language
|
|
17
|
+
|
|
18
|
+
1. Add its row to `languages.tsv` (keep it sorted).
|
|
19
|
+
2. Add its code to its script(s) in `scripts.tsv`.
|
|
20
|
+
3. If the script has other languages: add `alphabets.tsv` letters and a
|
|
21
|
+
`trigrams/<code>.txt` profile (`rake psychowl:train[code]` builds one from
|
|
22
|
+
`corpus/<code>.txt`).
|
|
23
|
+
4. Add samples to `core/tests/samples/<code>.txt`; both test suites pick
|
|
24
|
+
them up.
|
|
25
|
+
|
|
26
|
+
## Provenance
|
|
27
|
+
|
|
28
|
+
The initial profiles, alphabets and script ranges were ported from
|
|
29
|
+
[whatlang](https://github.com/greyblake/whatlang-rs) 0.18.0 with
|
|
30
|
+
`tools/import_whatlang.rb`; see `LICENSE-whatlang` (MIT). Changes from the
|
|
31
|
+
original: the Latin range no longer overlaps the Cyrillic letters U+1D2B and
|
|
32
|
+
U+1D78, and Mandarin is named by its Unicode script name, Han.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Letters commonly used by a language, lowercase. Only used for scripts
|
|
2
|
+
# shared by several languages (Latin, Cyrillic).
|
|
3
|
+
# code letters
|
|
4
|
+
deu abcdefghijklmnopqrstuvwxyzßäöü
|
|
5
|
+
eng abcdefghijklmnopqrstuvwxyz
|
|
6
|
+
fra abcdefghijklmnopqrstuvwxyzàâçèéêëîïôùûüÿœ
|
|
7
|
+
ind abcdefghijklmnopqrstuvwxyz
|
|
8
|
+
ita abcdefghijklmnopqrstuvwxyzàèéìòù
|
|
9
|
+
nld abcdefghijklmnopqrstuvwxyzàèéëïij
|
|
10
|
+
pol abcdefghijklmnopqrstuvwxyzóąćęłńśźż
|
|
11
|
+
por abcdefghijklmnopqrstuvwxyzàáâãçéêíóôõú
|
|
12
|
+
rus абвгдежзийклмнопрстуфхцчшщъыьэюяё
|
|
13
|
+
spa abcdefghijklmnopqrstuvwxyz¡¿áéíñóúü
|
|
14
|
+
tur abcdefghijklmnopqrstuvwxyzçöüğış̇
|
|
15
|
+
ukr абвгдежзийклмнопрстуфхцчшщьюяєіїґ
|
|
16
|
+
vie abcdefghijklmnopqrstuvwxyzàáâãèéêìíòóôõùúýăđĩũơưạảấầẩẫậắằẳẵặẹẻẽếềểễệỉịọỏốồổỗộớờởỡợụủứừửữựỳỵỷỹ
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Supported languages, sorted by code. Line order defines language indices.
|
|
2
|
+
# code (ISO 639-3) ISO 639-1 English name native name
|
|
3
|
+
ara ar Arabic العربية
|
|
4
|
+
cmn zh Mandarin 普通话
|
|
5
|
+
deu de German Deutsch
|
|
6
|
+
eng en English English
|
|
7
|
+
fra fr French Français
|
|
8
|
+
hin hi Hindi हिन्दी
|
|
9
|
+
ind id Indonesian Bahasa Indonesia
|
|
10
|
+
ita it Italian Italiano
|
|
11
|
+
jpn ja Japanese 日本語
|
|
12
|
+
kor ko Korean 한국어
|
|
13
|
+
nld nl Dutch Nederlands
|
|
14
|
+
pol pl Polish Polski
|
|
15
|
+
por pt Portuguese Português
|
|
16
|
+
rus ru Russian Русский
|
|
17
|
+
spa es Spanish Español
|
|
18
|
+
tur tr Turkish Türkçe
|
|
19
|
+
ukr uk Ukrainian Українська
|
|
20
|
+
vie vi Vietnamese Tiếng Việt
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Writing systems in detection priority order (ties go to the earlier one).
|
|
2
|
+
# Code point ranges are disjoint. "-" means no supported language yet.
|
|
3
|
+
# name languages ranges (hex, inclusive)
|
|
4
|
+
Latin deu,eng,fra,ind,ita,nld,pol,por,spa,tur,vie 0061-007A,0041-005A,0080-00FF,0100-017F,0180-024F,0250-02AF,1D00-1D2A,1D2C-1D77,1D79-1D7F,1D80-1DBF,1E00-1EFF,2100-214F,2C60-2C7F,A720-A7FF,AB30-AB6F
|
|
5
|
+
Cyrillic rus,ukr 0400-0484,0487-052F,2DE0-2DFF,A640-A69D,1D2B,1D78,A69F
|
|
6
|
+
Arabic ara 0600-06FF,0750-07FF,08A0-08FF,FB50-FDFF,FE70-FEFF,10E60-10E7F,1EE00-1EEFF
|
|
7
|
+
Han cmn,jpn 2E80-2E99,2E9B-2EF3,2F00-2FD5,3005,3007,3021-3029,3038-303B,3400-4DB5,4E00-9FCC,F900-FA6D,FA70-FAD9
|
|
8
|
+
Devanagari hin 0900-097F,A8E0-A8FF,1CD0-1CFF
|
|
9
|
+
Hebrew - 0590-05FF
|
|
10
|
+
Ethiopic - 1200-139F,2D80-2DDF,AB00-AB2F
|
|
11
|
+
Georgian - 10A0-10FF
|
|
12
|
+
Bengali - 0980-09FF
|
|
13
|
+
Hangul kor AC00-D7AF,1100-11FF,3130-318F,3200-32FF,A960-A97F,D7B0-D7FF,FF00-FFEF
|
|
14
|
+
Hiragana jpn 3040-309F
|
|
15
|
+
Katakana jpn 30A0-30FF
|
|
16
|
+
Greek - 0370-03FF
|
|
17
|
+
Kannada - 0C80-0CFF
|
|
18
|
+
Tamil - 0B80-0BFF
|
|
19
|
+
Thai - 0E00-0E7F
|
|
20
|
+
Gujarati - 0A80-0AFF
|
|
21
|
+
Gurmukhi - 0A00-0A7F
|
|
22
|
+
Telugu - 0C00-0C7F
|
|
23
|
+
Malayalam - 0D00-0D7F
|
|
24
|
+
Oriya - 0B00-0B7F
|
|
25
|
+
Myanmar - 1000-109F
|
|
26
|
+
Sinhala - 0D80-0DFF
|
|
27
|
+
Khmer - 1780-17FF,19E0-19FF
|
|
28
|
+
Armenian - 0530-058F,FB13-FB17
|