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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +21 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +212 -0
  5. data/exe/psychowl +106 -0
  6. data/ext/psychowl_native/core/data/LICENSE-whatlang +26 -0
  7. data/ext/psychowl_native/core/data/README.md +32 -0
  8. data/ext/psychowl_native/core/data/alphabets.tsv +16 -0
  9. data/ext/psychowl_native/core/data/languages.tsv +20 -0
  10. data/ext/psychowl_native/core/data/scripts.tsv +28 -0
  11. data/ext/psychowl_native/core/data/trigrams/ara.txt +302 -0
  12. data/ext/psychowl_native/core/data/trigrams/deu.txt +302 -0
  13. data/ext/psychowl_native/core/data/trigrams/eng.txt +302 -0
  14. data/ext/psychowl_native/core/data/trigrams/fra.txt +302 -0
  15. data/ext/psychowl_native/core/data/trigrams/hin.txt +302 -0
  16. data/ext/psychowl_native/core/data/trigrams/ind.txt +302 -0
  17. data/ext/psychowl_native/core/data/trigrams/ita.txt +302 -0
  18. data/ext/psychowl_native/core/data/trigrams/nld.txt +302 -0
  19. data/ext/psychowl_native/core/data/trigrams/pol.txt +302 -0
  20. data/ext/psychowl_native/core/data/trigrams/por.txt +302 -0
  21. data/ext/psychowl_native/core/data/trigrams/rus.txt +302 -0
  22. data/ext/psychowl_native/core/data/trigrams/spa.txt +302 -0
  23. data/ext/psychowl_native/core/data/trigrams/tur.txt +302 -0
  24. data/ext/psychowl_native/core/data/trigrams/ukr.txt +302 -0
  25. data/ext/psychowl_native/core/data/trigrams/vie.txt +302 -0
  26. data/lib/psychowl/active_model.rb +64 -0
  27. data/lib/psychowl/data_file.rb +14 -0
  28. data/lib/psychowl/detector.rb +123 -0
  29. data/lib/psychowl/engine.rb +23 -0
  30. data/lib/psychowl/info.rb +39 -0
  31. data/lib/psychowl/lang.rb +55 -0
  32. data/lib/psychowl/locale/en.yml +6 -0
  33. data/lib/psychowl/lookup.rb +25 -0
  34. data/lib/psychowl/native_speedup.rb +46 -0
  35. data/lib/psychowl/postgres.rb +35 -0
  36. data/lib/psychowl/railtie.rb +10 -0
  37. data/lib/psychowl/ruby_engine/segmentation.rb +84 -0
  38. data/lib/psychowl/ruby_engine/trigrams.rb +74 -0
  39. data/lib/psychowl/ruby_engine.rb +166 -0
  40. data/lib/psychowl/script.rb +39 -0
  41. data/lib/psychowl/segment.rb +24 -0
  42. data/lib/psychowl/tables.rb +55 -0
  43. data/lib/psychowl/text.rb +26 -0
  44. data/lib/psychowl/version.rb +5 -0
  45. data/lib/psychowl.rb +82 -0
  46. 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