kabosu 0.6.11.4-aarch64-linux → 0.7.0.1-aarch64-linux

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: f280c805cbd2980833e2209d61058c86491e825f3effa0b0d00fe12dc1c29b36
4
- data.tar.gz: 6fb1f2755f2d49ff2c6e28ba529dfe2339a21479475167b3863f07705526e913
3
+ metadata.gz: ba6c61184a10e59d12f7618f0769f27766422c11644ec5c47af51f7eb1775890
4
+ data.tar.gz: 1442e054d02e0bbfaad77d0447f32f33b00d8dec19aafd4c9f60062b0b1e11d4
5
5
  SHA512:
6
- metadata.gz: a5f0e29a22aedd8414662ac4d57469e6285454d7a45d7531d509231ea9062f7eeb77f07fb3ab36c768af81565d7c40f5f2fc60c4bf21b08ba76bfab288331b84
7
- data.tar.gz: a23470c82e400dba78b37c799958cf7e945ce4331d5b76999fcf28e49b895cde282326c6c6de54a91e6db9f801218f4bca942edba459e81318e46cc9dd0b8c1e
6
+ metadata.gz: cc59f91c5486eebb6382f68df24459d6dc34aa52c02e53197d6538c5e19830c9865a6c859535d2e95d8f082ea5beb928e0106ed6c5b7afd3a7a66220cbc847c1
7
+ data.tar.gz: f187ffa82f345736a0a160b4146ed6c9b6b7f4c69bfa98a03a76d80075c6670554a76063cffb3ba724d38bd3b237fdf8871a8c540eee81aa70b7168d2dfdfc46
data/README.md CHANGED
@@ -43,10 +43,12 @@ morpheme.dictionary_id # => 0 - source dictionary id
43
43
  morpheme.word_id # => 544373 - internal word id
44
44
  morpheme.synonym_group_ids # => [] - synonym group ids
45
45
  morpheme.dictionary_form_word_id # => -1 - dictionary-form word id
46
- morpheme.head_word_length # => 3 - head word length in codepoints
46
+ morpheme.head_word_length # => 9 - bytes of the normalized index form
47
47
  morpheme.a_unit_split # => [123, 456] - split-A word ids
48
48
  morpheme.b_unit_split # => [] - split-B word ids
49
49
  morpheme.word_structure # => [123, 456] - word-structure ids
50
+ morpheme.c_unit_split # => [] - split-C word ids (V1 dictionaries)
51
+ morpheme.user_data # => "" - free text from a user dictionary's user_data column
50
52
  morpheme.total_cost # => 5765 - morphological analysis cost
51
53
  morpheme.begin # => 0 - start byte offset
52
54
  morpheme.end # => 9 - end byte offset
@@ -75,22 +77,34 @@ Then install and download a [Sudachi dictionary](https://github.com/WorksApplica
75
77
 
76
78
  ```sh
77
79
  bundle install
78
- bundle exec rake kabosu:install[small] # or core, full
80
+ bundle exec rake kabosu:install:small # or :core, :full
79
81
  ```
80
82
 
81
83
  Dictionary editions (from smallest to largest): `small`, `core`, `full`. See the [SudachiDict documentation](https://github.com/WorksApplications/SudachiDict?tab=readme-ov-file#dictionary-types) for details on the differences between editions.
82
84
 
85
+ kabosu 0.7 (sudachi.rs 0.7) reads only the V1 dictionary format: SudachiDict `20261015` or later.
86
+
87
+ ## Upgrading from 0.6
88
+
89
+ sudachi.rs 0.7 changed the binary dictionary format, so a 0.6 setup needs three things:
90
+
91
+ 1. **Reinstall the system dictionary.** Dictionaries installed by kabosu 0.6 are V0 and cannot be loaded. Run `rake kabosu:install` (or `install_if_missing`, which ignores V0 files), then drop the old ones with `rake kabosu:remove VERSION=...`. `rake kabosu:list` marks them.
92
+ 2. **Rebuild user dictionaries** against that exact system dictionary: `rake kabosu:build_user_dict CSV=names.csv OUTPUT=names.dic` (or `Kabosu.build_user_dict`). A user dictionary now records which system dictionary it was built with, and loading it with a different one raises `Kabosu::DictionaryError`. The CSV uses the V1 column layout (see [Sudachi's user dictionary guide](https://github.com/WorksApplications/Sudachi/blob/develop/docs/user_dict.md)).
93
+ 3. **Expect small tokenization differences.** sudachi.rs 0.7 aligned its lattice search, character-category handling and katakana OOV joining with Java Sudachi, so some inputs segment differently even with the same dictionary. Custom `char.def` files must replace `NOOOVBOW2` with `NOOOVBOW NOOOVEOW`.
94
+
95
+ Other visible changes: `Dictionary#lookup` normalizes the query first (`カボス` finds カボス), joined numerals and katakana carry an OOV `word_id` instead of `4294967295`, and a custom config that names no `char.def`/`unk.def` falls back to the files built into sudachi.rs.
96
+
83
97
  ## Dictionary management
84
98
 
85
99
  Rake tasks for managing Sudachi dictionaries:
86
100
 
87
101
  ```sh
88
- rake kabosu:install[small] # Install a dictionary (VERSION=YYYYMMDD for a specific version)
102
+ rake kabosu:install:small # Install a dictionary (VERSION=YYYYMMDD for a specific version)
89
103
  rake kabosu:install_if_missing # Same, but a no-op when a dictionary is already installed
90
104
  rake kabosu:list # List installed dictionaries
91
105
  rake kabosu:versions # Show available versions from GitHub
92
106
  rake kabosu:path # Show path to best available dictionary
93
- rake kabosu:remove[small] # Remove a dictionary (VERSION=YYYYMMDD for a specific version)
107
+ rake kabosu:remove:small # Remove a dictionary (VERSION=YYYYMMDD for a specific version)
94
108
  ```
95
109
 
96
110
  Dictionaries are stored in `~/.kabosu/dict/` by default. Set `KABOSU_DICT_DIR` to customize — useful for pointing at a Docker volume so the dictionary persists across deployments.
@@ -134,7 +148,7 @@ list = tokenizer.tokenize("国会議事堂前駅")
134
148
  list.surfaces
135
149
  list.first.part_of_speech
136
150
 
137
- # Dictionary prefix lookup
151
+ # Dictionary prefix lookup (the query is normalized first, like tokenization)
138
152
  dict.lookup("東京都").surfaces
139
153
 
140
154
  # Morpheme split
@@ -146,6 +160,31 @@ Kabosu.split_sentences("東京都に住んでいる。大阪も好きだ。", ra
146
160
  Kabosu.split_sentences("長い文...", limit: 12, with_checker: true)
147
161
  ```
148
162
 
163
+ ### Dictionary entries and forms (sudachi.rs 0.7)
164
+
165
+ ```ruby
166
+ m = tokenizer.tokenize("住んでいた").first # 住ん
167
+ m.dictionary_form_morpheme # the 住む entry itself: POS, reading, ids…
168
+ tokenizer.tokenize("附属").first.normalized_form_morpheme.surface # => "付属"
169
+ # A word that is its own form (and any OOV word) returns itself.
170
+
171
+ # Every entry in the loaded dictionaries, lazily (millions for `full`)
172
+ dict.entries.lazy.select(&:user?).first(10)
173
+
174
+ # Exact matches by scanning every entry, including ones not indexed for
175
+ # lookup. Slow on large dictionaries; prefer #lookup.
176
+ dict.lookup_all_entries("カボス").surfaces # => ["カボス"]
177
+
178
+ # Hand-made out-of-vocabulary morpheme; forms default to the surface
179
+ dict.oov_morpheme(m.part_of_speech_id, "ズィーガン", reading: "ジーガン")
180
+
181
+ # What tokenization actually sees after the input-text plugins
182
+ dict.normalize("カボス ABC コンピュータ〜〜") # => "カボス abc コンピューター"
183
+
184
+ # Compile a user dictionary against the installed system dictionary
185
+ Kabosu.build_user_dict("names.csv", output: "names.dic")
186
+ ```
187
+
149
188
  ## Benchmarks
150
189
 
151
190
  Kabosu ships with a benchmark suite that measures tokenization throughput and compares the Ruby bindings against raw [sudachi.rs](https://github.com/WorksApplications/sudachi.rs).
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -10,9 +10,16 @@ module Kabosu
10
10
  EDITION_PRIORITY = %w[full core small].freeze
11
11
  GITHUB_REPO = "WorksApplications/SudachiDict".freeze
12
12
  GITHUB_API = "https://api.github.com".freeze
13
+ # sudachi.rs 0.7 reads only V1 dictionaries, which open with this magic.
14
+ # Older (V0) files stay listed so they can be removed, but are never picked.
15
+ V1_MAGIC = "SudachiBinaryDic".b.freeze
16
+ # The first SudachiDict GitHub release carrying V1 assets. v20260723.1 is
17
+ # V1 too, but was published to PyPI only and has no assets to download.
18
+ MIN_VERSION = "20261015".freeze
13
19
 
14
20
  class DictNotFound < StandardError; end
15
21
  class DownloadError < StandardError; end
22
+ class UnsupportedVersion < StandardError; end
16
23
 
17
24
  # Default storage directory. Honors KABOSU_DICT_DIR so consumers can point
18
25
  # the gem at a Docker volume / shared mount without subclassing or threading
@@ -37,13 +44,19 @@ module Kabosu
37
44
  def install(edition = "core", version: nil)
38
45
  edition = validate_edition(edition)
39
46
  version ||= latest_version
47
+ validate_version(version)
40
48
 
41
49
  dest_dir = File.join(@dir, "sudachi-dictionary-#{version}")
42
50
  dic_path = File.join(dest_dir, "system_#{edition}.dic")
43
51
 
44
52
  if File.exist?(dic_path)
45
- warn "Already installed: #{dic_path}"
46
- return dic_path
53
+ if v1?(dic_path)
54
+ warn "Already installed: #{dic_path}"
55
+ return dic_path
56
+ end
57
+
58
+ warn "Replacing pre-V1 dictionary: #{dic_path}"
59
+ FileUtils.rm_f(dic_path)
47
60
  end
48
61
 
49
62
  FileUtils.mkdir_p(@dir)
@@ -73,6 +86,11 @@ module Kabosu
73
86
 
74
87
  raise DownloadError, "Expected #{dic_path} after extraction, but file not found" unless File.exist?(dic_path)
75
88
 
89
+ unless v1?(dic_path)
90
+ FileUtils.rm_f(dic_path)
91
+ raise UnsupportedVersion, "#{version}/#{edition} is not a V1 dictionary, which sudachi.rs 0.7+ requires"
92
+ end
93
+
76
94
  warn "Installed: #{dic_path}"
77
95
  dic_path
78
96
  end
@@ -87,7 +105,7 @@ module Kabosu
87
105
  #
88
106
  def install_if_missing(edition = "core", version: nil)
89
107
  edition = validate_edition(edition)
90
- matching = installed.find do |d|
108
+ matching = readable.find do |d|
91
109
  d[:edition] == edition && (version.nil? || d[:version] == version)
92
110
  end
93
111
  return matching[:path] if matching
@@ -121,8 +139,13 @@ module Kabosu
121
139
  # Find the best available dictionary path.
122
140
  # Prefers: latest version, then full > core > small.
123
141
  def find(edition: nil)
124
- candidates = installed
125
- raise DictNotFound, "No dictionaries installed. Run: rake kabosu:install" if candidates.empty?
142
+ candidates = readable
143
+ if candidates.empty?
144
+ raise DictNotFound, "No dictionaries installed. Run: rake kabosu:install" if installed.empty?
145
+
146
+ raise DictNotFound, "Only pre-V1 dictionaries are installed, which sudachi.rs 0.7+ cannot read. " \
147
+ "Run: rake kabosu:install (and rake kabosu:remove VERSION=... for the old ones)"
148
+ end
126
149
 
127
150
  if edition
128
151
  edition = validate_edition(edition)
@@ -144,6 +167,14 @@ module Kabosu
144
167
  best.is_a?(Hash) ? best[:path] : latest_version_dicts.first[:path]
145
168
  end
146
169
 
170
+ def readable
171
+ installed.select { |d| v1?(d[:path]) }
172
+ end
173
+
174
+ def v1?(path)
175
+ File.binread(path, V1_MAGIC.bytesize) == V1_MAGIC
176
+ end
177
+
147
178
  # ── Remove ──
148
179
 
149
180
  # Remove a specific dictionary edition, or an entire version.
@@ -189,6 +220,15 @@ module Kabosu
189
220
 
190
221
  private
191
222
 
223
+ def validate_version(version)
224
+ return if Gem::Version.new(version) >= Gem::Version.new(MIN_VERSION)
225
+
226
+ raise UnsupportedVersion, "SudachiDict #{version} predates the V1 format sudachi.rs 0.7+ reads; " \
227
+ "use #{MIN_VERSION} or later"
228
+ rescue ArgumentError
229
+ raise UnsupportedVersion, "Malformed SudachiDict version: #{version.inspect}"
230
+ end
231
+
192
232
  def validate_edition(edition)
193
233
  edition = edition.to_s.downcase
194
234
  unless EDITIONS.include?(edition)
@@ -174,6 +174,6 @@ CYRILLIC 1 1 0
174
174
  0x1F3FB..0x1F3FE ALL NOOOVBOW # emoji skin tone modifiers https://codepoints.net/U+1F3FF
175
175
 
176
176
  # Combination marks
177
- 0x200C..0x200D ALL NOOOVBOW2 # Zero Width Non-Joiner/Joiner
177
+ 0x200C..0x200D ALL NOOOVBOW NOOOVEOW # Zero Width Non-Joiner/Joiner
178
178
 
179
179
  # END OF TABLE
@@ -56,6 +56,19 @@ namespace :kabosu do
56
56
  end
57
57
  end
58
58
 
59
+ # ── User dictionaries ──
60
+
61
+ desc "Build a user dictionary: CSV=a.csv[,b.csv] OUTPUT=user.dic [SYSTEM_DICT=path] " \
62
+ "(defaults to the best installed dictionary)"
63
+ task :build_user_dict do
64
+ require "kabosu"
65
+ csv = ENV.fetch("CSV") { abort "CSV=path/to/lexicon.csv is required" }.split(",")
66
+ output = ENV.fetch("OUTPUT") { abort "OUTPUT=path/to/user.dic is required" }
67
+ system_dict = ENV.fetch("SYSTEM_DICT") { Kabosu::Dictionary.path }
68
+ Kabosu.build_user_dict(csv, output: output, system_dict: system_dict)
69
+ puts "Built #{output} against #{system_dict}"
70
+ end
71
+
59
72
  # ── Info ──
60
73
 
61
74
  desc "List installed Sudachi dictionaries"
@@ -70,7 +83,8 @@ namespace :kabosu do
70
83
  puts
71
84
  dicts.each do |d|
72
85
  size_mb = (File.size(d[:path]).to_f / 1024 / 1024).round(1)
73
- puts " #{d[:version]} / #{d[:edition].ljust(5)} (#{size_mb} MB)"
86
+ unreadable = manager.v1?(d[:path]) ? "" : " [pre-V1, unreadable by this kabosu]"
87
+ puts " #{d[:version]} / #{d[:edition].ljust(5)} (#{size_mb} MB)#{unreadable}"
74
88
  puts " #{d[:path]}"
75
89
  end
76
90
  end
@@ -1,3 +1,3 @@
1
1
  module Kabosu
2
- VERSION = "0.6.11.4".freeze
2
+ VERSION = "0.7.0.1".freeze
3
3
  end
data/lib/kabosu.rb CHANGED
@@ -105,6 +105,14 @@ module Kabosu
105
105
 
106
106
  def map_dictionary_init_error(error, config:, system_dict:)
107
107
  message = error.message
108
+ if message.include?("V0 version")
109
+ return DictionaryError.new(
110
+ "#{message}: a system or user dictionary is in the pre-V1 format, which sudachi.rs 0.7+ cannot read. " \
111
+ "Install SudachiDict #{DictManager::MIN_VERSION} or later (rake kabosu:install) " \
112
+ "and rebuild user dictionaries against it."
113
+ )
114
+ end
115
+
108
116
  if (config && system_dict.nil?) || message.match?(/config|setting\.json|json/i)
109
117
  ConfigError.new(message)
110
118
  else
@@ -148,6 +156,41 @@ module Kabosu
148
156
  rescue RuntimeError => e
149
157
  raise LookupError.new(e.message), cause: e
150
158
  end
159
+
160
+ # Every public dictionary entry as a standalone Morpheme (offsets cover its
161
+ # own headword). Lazy: a `full` dictionary holds millions of entries, so
162
+ # chain with `.lazy`, `.first(n)` or `.each` rather than `.to_a`.
163
+ #
164
+ # dict.entries.lazy.select { _1.user? }.first(10)
165
+ def entries(&block)
166
+ return enum_for(:entries) unless block
167
+
168
+ _each_entry(&block)
169
+ self
170
+ end
171
+
172
+ # Like #lookup, but exact matches only and found by scanning every entry,
173
+ # so it also finds entries that are not indexed for lookup. Slow.
174
+ def lookup_all_entries(surface)
175
+ raise ArgumentError, "surface must be a String" unless surface.is_a?(String)
176
+
177
+ MorphemeList.new(_lookup_all_entries(surface))
178
+ rescue RuntimeError => e
179
+ raise LookupError.new(e.message), cause: e
180
+ end
181
+
182
+ # Builds an out-of-vocabulary Morpheme by hand. The forms default to the
183
+ # surface. pos_id comes from Morpheme#part_of_speech_id.
184
+ #
185
+ # dict.oov_morpheme(pos_id, "ズィーガンボルト", reading: "ズィーガンボルト")
186
+ def oov_morpheme(pos_id, surface, reading: nil, normalized_form: nil, dictionary_form: nil)
187
+ raise ArgumentError, "pos_id must be an Integer" unless pos_id.is_a?(Integer)
188
+ raise ArgumentError, "surface must be a String" unless surface.is_a?(String)
189
+
190
+ _oov_morpheme(pos_id, surface, reading, normalized_form, dictionary_form)
191
+ rescue RangeError, RuntimeError => e
192
+ raise ArgumentError, "invalid part-of-speech id #{pos_id}: #{e.message}"
193
+ end
151
194
  end
152
195
 
153
196
  # ── Tokenizer: wrap output in MorphemeList ──
@@ -179,6 +222,23 @@ module Kabosu
179
222
  end
180
223
  end
181
224
 
225
+ # Compiles user-dictionary CSV(s) into a binary user dictionary. The result
226
+ # loads only next to the system dictionary it was built against, so rebuild
227
+ # whenever that changes (including the move to sudachi.rs 0.7).
228
+ #
229
+ # Kabosu.build_user_dict("names.csv", output: "names.dic")
230
+ # Kabosu::Dictionary.new(system_dict: Kabosu::Dictionary.path, user_dicts: ["names.dic"])
231
+ def self.build_user_dict(inputs, output:, system_dict: Dictionary.path, description: nil)
232
+ inputs = Array(inputs)
233
+ raise ArgumentError, "inputs must be String paths" unless inputs.all?(String)
234
+ raise ArgumentError, "output must be a String" unless output.is_a?(String)
235
+
236
+ _build_user_dict(inputs, system_dict, output, description)
237
+ output
238
+ rescue RuntimeError => e
239
+ raise DictionaryError.new(e.message), cause: e
240
+ end
241
+
182
242
  def self.split_sentences(text, limit: nil, with_checker: false, ranges: false, dictionary: nil)
183
243
  raise ArgumentError, "text must be a String" unless text.is_a?(String)
184
244
  raise ArgumentError, "limit must be an Integer or nil" unless limit.nil? || limit.is_a?(Integer)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kabosu
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.11.4
4
+ version: 0.7.0.1
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - davafons
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-13 00:00:00.000000000 Z
11
+ date: 2026-11-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rubyzip