static_embeddings 0.1.4 → 0.1.5

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.
@@ -3,7 +3,7 @@ require "digest"
3
3
  module StaticEmbeddings
4
4
  module Format
5
5
  MAGIC = "SEMBv1\0\0"
6
- VERSION = 2
6
+ VERSION = 3
7
7
  HEADER_SIZE = 320
8
8
  ALIGNMENT = 64
9
9
 
@@ -12,7 +12,7 @@ module StaticEmbeddings
12
12
  POOLING_MEAN = 1
13
13
  NORMALIZATION_NONE = 0
14
14
  NORMALIZATION_L2 = 1
15
- TRUNCATE_IDS_BEFORE_POOLING = 1
15
+ TRUNCATE_USABLE_IDS_BEFORE_POOLING = 2
16
16
  UNK_INCLUDE = 0
17
17
  UNK_DROP = 1
18
18
  EMPTY_ZERO_VECTOR = 0
@@ -26,6 +26,14 @@ module StaticEmbeddings
26
26
  CHECKSUM_OFFSET = 240
27
27
  CHECKSUM_SIZE = 32
28
28
  MAX_PROBE_OFFSET = 304
29
+ ADDED_TOKEN_MASK_OFFSET = 308
30
+
31
+ ADDED_PAD = 1 << 0
32
+ ADDED_UNK = 1 << 1
33
+ ADDED_CLS = 1 << 2
34
+ ADDED_SEP = 1 << 3
35
+ ADDED_MASK = 1 << 4
36
+ ADDED_TOKEN_MASK_ALL = ADDED_PAD | ADDED_UNK | ADDED_CLS | ADDED_SEP | ADDED_MASK
29
37
 
30
38
  SECTION_FIELDS = {
31
39
  vocab_strings: 128,
@@ -44,7 +52,7 @@ module StaticEmbeddings
44
52
  28 => TOKENIZER_BERT_WORDPIECE_V1,
45
53
  32 => DTYPE_F32,
46
54
  36 => POOLING_MEAN,
47
- 48 => TRUNCATE_IDS_BEFORE_POOLING,
55
+ 48 => TRUNCATE_USABLE_IDS_BEFORE_POOLING,
48
56
  108 => HASH_SEED
49
57
  }.freeze
50
58
 
@@ -102,7 +110,7 @@ module StaticEmbeddings
102
110
  def write(path:, meta:, tokens:, matrix:, norm_tables:, provenance:)
103
111
  hash_size, strings, hash_blob, max_probe = build_hash_table(tokens)
104
112
  root_trie, continuation_trie = build_wordpiece_tries(tokens, meta.fetch(:subword_prefix))
105
- sections, body = build_body(
113
+ payloads = {
106
114
  vocab_strings: strings,
107
115
  vocab_hash: hash_blob,
108
116
  embeddings: matrix,
@@ -110,15 +118,48 @@ module StaticEmbeddings
110
118
  provenance: provenance,
111
119
  root_trie: root_trie,
112
120
  continuation_trie: continuation_trie
113
- )
114
-
121
+ }
122
+ sections, file_size = layout_sections(payloads)
115
123
  header = build_header(meta, tokens.length, hash_size, max_probe, sections)
116
- file = header << body
117
- digest = Digest::SHA256.digest(file)
118
- file[CHECKSUM_OFFSET, CHECKSUM_SIZE] = digest
119
124
 
120
- File.binwrite(path, file)
121
- { bytes: file.bytesize, sha256: digest.unpack1("H*"), hash_table_size: hash_size }
125
+ digest = Digest::SHA256.new
126
+ File.open(path, "wb") do |io|
127
+ io.write(header)
128
+ digest << header
129
+ offset = HEADER_SIZE
130
+
131
+ payloads.each do |name, payload|
132
+ target = sections.fetch(name).first
133
+ padding = target - offset
134
+ if padding.positive?
135
+ zeros = "\0".b * padding
136
+ io.write(zeros)
137
+ digest << zeros
138
+ end
139
+ write_payload(io, digest, payload)
140
+ offset = target + payload.bytesize
141
+ end
142
+ end
143
+
144
+ checksum = digest.digest
145
+ File.open(path, "r+b") do |io|
146
+ io.seek(CHECKSUM_OFFSET, IO::SEEK_SET)
147
+ io.write(checksum)
148
+ end
149
+
150
+ { bytes: file_size, sha256: checksum.unpack1("H*"), hash_table_size: hash_size }
151
+ end
152
+
153
+ def write_payload(io, digest, payload)
154
+ if payload.respond_to?(:each_chunk)
155
+ payload.each_chunk do |chunk|
156
+ io.write(chunk)
157
+ digest << chunk
158
+ end
159
+ else
160
+ io.write(payload)
161
+ digest << payload
162
+ end
122
163
  end
123
164
 
124
165
  def verify(path)
@@ -240,20 +281,16 @@ module StaticEmbeddings
240
281
  end
241
282
  end
242
283
 
243
- def build_body(payloads)
244
- body = binary_string
284
+ def layout_sections(payloads)
285
+ offset = HEADER_SIZE
245
286
  sections = {}
246
287
  payloads.each do |name, payload|
247
- align_body!(body)
248
- sections[name] = [HEADER_SIZE + body.bytesize, payload.bytesize]
249
- body << payload
288
+ padding = (ALIGNMENT - (offset % ALIGNMENT)) % ALIGNMENT
289
+ offset += padding
290
+ sections[name] = [offset, payload.bytesize]
291
+ offset += payload.bytesize
250
292
  end
251
- [sections, body]
252
- end
253
-
254
- def align_body!(body)
255
- padding = (ALIGNMENT - ((HEADER_SIZE + body.bytesize) % ALIGNMENT)) % ALIGNMENT
256
- body << "\0".b * padding if padding.positive?
293
+ [sections, offset]
257
294
  end
258
295
 
259
296
  def build_header(meta, vocab_size, hash_size, max_probe, sections)
@@ -268,6 +305,7 @@ module StaticEmbeddings
268
305
  put_u32(header, 104, hash_size)
269
306
  put_u32(header, MAX_TOKEN_CHARS_OFFSET, meta.fetch(:max_token_chars))
270
307
  put_u32(header, MAX_PROBE_OFFSET, max_probe)
308
+ put_u32(header, ADDED_TOKEN_MASK_OFFSET, meta.fetch(:added_token_mask, 0))
271
309
  put_prefix(header, meta.fetch(:subword_prefix))
272
310
  SECTION_FIELDS.each { |name, field| put_section(header, field, sections.fetch(name)) }
273
311
 
@@ -11,7 +11,24 @@ module StaticEmbeddings
11
11
  end
12
12
 
13
13
  def model_path(model_id, env = ENV)
14
- File.join(cache_dir(env), "models", "#{model_id}.semb")
14
+ id = model_id.to_s
15
+ raise ArgumentError, "model_id must not be empty" if id.empty?
16
+ raise ArgumentError, "model_id contains a NUL byte" if id.include?("\0")
17
+
18
+ normalized = id.tr("\\", "/")
19
+ parts = normalized.split("/", -1)
20
+ if normalized.start_with?("/") ||
21
+ parts.any? { |part| part.empty? || part == "." || part == ".." || part.include?(":") }
22
+ raise ArgumentError, "model_id must be a relative slash-separated identifier"
23
+ end
24
+
25
+ base = File.expand_path(File.join(cache_dir(env), "models"))
26
+ path = File.expand_path(File.join(base, "#{normalized}.semb"))
27
+ prefix = base.end_with?(File::SEPARATOR) ? base : "#{base}#{File::SEPARATOR}"
28
+ unless path.start_with?(prefix)
29
+ raise ArgumentError, "model_id escapes the model cache"
30
+ end
31
+ path
15
32
  end
16
33
 
17
34
  def builtin_path(name)
@@ -1,14 +1,18 @@
1
+ require "json"
2
+ require "static_embeddings/converter"
3
+ require "static_embeddings/safetensors"
4
+
1
5
  module StaticEmbeddings
2
6
  class Reference
3
7
  CJK_RANGES = [
4
8
  0x4E00..0x9FFF, 0x3400..0x4DBF, 0x20000..0x2A6DF, 0x2A700..0x2B73F,
5
- 0x2B740..0x2B81F, 0x2B820..0x2CEAF, 0xF900..0xFAFF, 0x2F800..0x2FA1F
9
+ 0x2B740..0x2B81F, 0x2B920..0x2CEAF, 0xF900..0xFAFF, 0x2F800..0x2FA1F
6
10
  ].freeze
7
11
 
8
12
  ASCII_PUNCT = [33..47, 58..64, 91..96, 123..126].freeze
9
13
  ASCII_SPACES = [" ", "\t", "\n", "\r"].freeze
10
14
  RE_PUNCT = /\A\p{P}\z/
11
- RE_CONTROL = /\A(?:\p{Cc}|\p{Cf}|\p{Co}|\p{Cs})\z/
15
+ RE_CONTROL = /\A(?:\p{Cc}|\p{Cf}|\p{Co})\z/
12
16
  RE_MN = /\A\p{Mn}\z/
13
17
  RE_WHITESPACE = /\A(?:\p{Zs}|[\u0085\u2028\u2029])\z/
14
18
 
@@ -29,7 +33,7 @@ module StaticEmbeddings
29
33
  tokens = tokens_from(tokenizer)
30
34
  tensor = Safetensors.read(File.join(dir, "model.safetensors"))[:tensors].values.first
31
35
  rows, dim = tensor[:shape]
32
- floats = tensor[:bytes].unpack("e*")
36
+ floats = Safetensors.f32_bytes(tensor).unpack("e*")
33
37
  normalizer = tokenizer["normalizer"] || {}
34
38
 
35
39
  new(tokens: tokens,
@@ -53,10 +57,21 @@ module StaticEmbeddings
53
57
  max_input_chars_per_word: tokenizer.dig("model", "max_input_chars_per_word") || 100,
54
58
  unk_token: tokenizer.dig("model", "unk_token") || "[UNK]",
55
59
  normalize: config.key?("normalize") ? config["normalize"] : true,
56
- max_tokens: max_tokens
60
+ max_tokens: max_tokens,
61
+ added_tokens: supported_added_tokens(tokenizer)
57
62
  }
58
63
  end
59
64
 
65
+ def self.supported_added_tokens(tokenizer)
66
+ allowed = Converter::STANDARD_SPECIAL_TOKENS
67
+ (tokenizer["added_tokens"] || []).each_with_object({}) do |token, out|
68
+ content = token["content"]
69
+ next unless token["special"] && allowed.key?(content) && token["normalized"] == false
70
+
71
+ out[content] = Integer(token.fetch("id"))
72
+ end
73
+ end
74
+
60
75
  def normalize_text(text)
61
76
  chars = text.chars
62
77
  chars = clean_chars(chars) if @meta[:clean_text]
@@ -71,13 +86,13 @@ module StaticEmbeddings
71
86
  end
72
87
 
73
88
  def tokenize(text, max_tokens: @meta[:max_tokens])
74
- ids = []
75
- pre_tokenize(normalize_text(text)).each { |word| ids.concat(wordpiece(word)) }
89
+ ids = tokenize_with_added_tokens(text)
76
90
  max_tokens && max_tokens.positive? && ids.length > max_tokens ? ids.first(max_tokens) : ids
77
91
  end
78
92
 
79
93
  def embed(text, max_tokens: @meta[:max_tokens])
80
- used = tokenize(text, max_tokens: max_tokens).reject { |id| id == unk_id }
94
+ used = tokenize(text, max_tokens: false).reject { |id| id == unk_id }
95
+ used = used.first(max_tokens) if max_tokens && max_tokens.positive? && used.length > max_tokens
81
96
  return Array.new(@dim, 0.0) if used.empty?
82
97
 
83
98
  vector = pooled(used)
@@ -86,6 +101,38 @@ module StaticEmbeddings
86
101
 
87
102
  private
88
103
 
104
+ def tokenize_with_added_tokens(text)
105
+ added = @meta[:added_tokens]
106
+ return tokenize_plain(text) if added.empty?
107
+
108
+ ids = []
109
+ cursor = 0
110
+ binary = text.b
111
+ while cursor < text.bytesize
112
+ match = added.keys.filter_map do |literal|
113
+ index = binary.index(literal.b, cursor)
114
+ index && [index, -literal.bytesize, literal]
115
+ end.min
116
+
117
+ unless match
118
+ ids.concat(tokenize_plain(text.byteslice(cursor, text.bytesize - cursor)))
119
+ break
120
+ end
121
+
122
+ index, _, literal = match
123
+ ids.concat(tokenize_plain(text.byteslice(cursor, index - cursor))) if index > cursor
124
+ ids << added.fetch(literal)
125
+ cursor = index + literal.bytesize
126
+ end
127
+ ids
128
+ end
129
+
130
+ def tokenize_plain(text)
131
+ return [] if text.nil? || text.empty?
132
+
133
+ pre_tokenize(normalize_text(text)).flat_map { |word| wordpiece(word) }
134
+ end
135
+
89
136
  def clean_chars(chars)
90
137
  chars.filter_map do |ch|
91
138
  cp = ch.ord
@@ -3,61 +3,157 @@ require "json"
3
3
  module StaticEmbeddings
4
4
  module Safetensors
5
5
  MAX_HEADER_BYTES = 100 * 1024 * 1024
6
- SUPPORTED_DTYPES = { "F32" => 4 }.freeze
6
+ SUPPORTED_DTYPES = { "F32" => 4, "F16" => 2, "BF16" => 2 }.freeze
7
+ CONVERT_CHUNK_ELEMENTS = 64 * 1024
8
+ IO_CHUNK_BYTES = 1024 * 1024
9
+
10
+ class F32Payload
11
+ attr_reader :bytesize
12
+
13
+ def initialize(path, tensor)
14
+ @path = path
15
+ @offset = tensor.fetch(:absolute_offset)
16
+ @source_bytes = tensor.fetch(:source_bytes)
17
+ @dtype = tensor.fetch(:dtype)
18
+ @bytesize = tensor.fetch(:shape).reduce(1) { |count, dim| count * dim } * 4
19
+ end
7
20
 
8
- module_function
21
+ def each_chunk
22
+ return enum_for(__method__) unless block_given?
23
+
24
+ File.open(@path, "rb") do |io|
25
+ io.seek(@offset, IO::SEEK_SET)
26
+ remaining = @source_bytes
27
+ while remaining.positive?
28
+ read_size = [remaining, IO_CHUNK_BYTES].min
29
+ # F16/BF16 elements are two bytes; never split one between chunks.
30
+ read_size -= 1 if @dtype != "F32" && read_size.odd?
31
+ read_size = remaining if read_size.zero?
32
+ chunk = io.read(read_size)
33
+ unless chunk&.bytesize == read_size
34
+ raise ConversionError, "tensor data is truncated while streaming #{@path}"
35
+ end
36
+ chunk.force_encoding(Encoding::BINARY)
37
+ yield case @dtype
38
+ when "F32" then chunk
39
+ when "F16" then Safetensors.convert_f16_to_f32(chunk)
40
+ when "BF16" then Safetensors.convert_bf16_to_f32(chunk)
41
+ else
42
+ raise ConversionError, "tensor dtype #{@dtype} is not supported"
43
+ end
44
+ remaining -= read_size
45
+ end
46
+ end
47
+ end
48
+ end
9
49
 
10
- def read(path)
11
- data = File.binread(path)
12
- header, body_offset = parse_header(data)
13
- body_size = data.bytesize - body_offset
50
+ module_function
14
51
 
15
- tensors = header.each_with_object({}) do |(name, spec), acc|
16
- next if name == "__metadata__"
52
+ def describe(path)
53
+ File.open(path, "rb") do |io|
54
+ file_size = io.stat.size
55
+ header, body_offset = read_header(io, file_size)
56
+ body_size = file_size - body_offset
57
+
58
+ tensors = header.each_with_object({}) do |(name, spec), acc|
59
+ next if name == "__metadata__"
60
+
61
+ dtype = spec["dtype"]
62
+ shape = spec["shape"]
63
+ begin_off, end_off = checked_offsets(name, spec["data_offsets"], body_size)
64
+ expected = checked_tensor_bytes(name, dtype, shape)
65
+ actual = end_off - begin_off
66
+ if actual != expected
67
+ raise ConversionError,
68
+ "tensor #{name}: shape #{shape.inspect} implies #{expected} bytes, offsets span #{actual}"
69
+ end
70
+
71
+ acc[name] = {
72
+ dtype: dtype,
73
+ shape: shape,
74
+ absolute_offset: body_offset + begin_off,
75
+ source_bytes: actual
76
+ }
77
+ end
78
+
79
+ { metadata: header["__metadata__"] || {}, tensors: tensors }
80
+ end
81
+ end
17
82
 
18
- acc[name] = tensor_from(data, body_offset, body_size, name, spec)
83
+ def read(path)
84
+ description = describe(path)
85
+ tensors = description.fetch(:tensors).transform_values do |tensor|
86
+ File.open(path, "rb") do |io|
87
+ io.seek(tensor.fetch(:absolute_offset), IO::SEEK_SET)
88
+ bytes = io.read(tensor.fetch(:source_bytes))
89
+ unless bytes&.bytesize == tensor.fetch(:source_bytes)
90
+ raise ConversionError, "tensor data is truncated while reading #{path}"
91
+ end
92
+ tensor.merge(bytes: bytes.force_encoding(Encoding::BINARY))
93
+ end
19
94
  end
95
+ { metadata: description.fetch(:metadata), tensors: tensors }
96
+ end
20
97
 
21
- { metadata: header["__metadata__"] || {}, tensors: tensors }
98
+ def f32_payload(path, tensor)
99
+ F32Payload.new(path, tensor)
22
100
  end
23
101
 
24
102
  def write(path, name, shape, floats)
25
103
  body = floats.pack("e*")
26
- header = JSON.generate(name => { "dtype" => "F32", "shape" => shape, "data_offsets" => [0, body.bytesize] })
104
+ write_raw(path, name, shape, "F32", body)
105
+ end
106
+
107
+ def write_raw(path, name, shape, dtype, body)
108
+ checked_tensor_bytes(name, dtype, shape).tap do |expected|
109
+ raise ArgumentError, "body size does not match #{dtype} shape" unless body.bytesize == expected
110
+ end
111
+
112
+ header = JSON.generate(name => { "dtype" => dtype, "shape" => shape, "data_offsets" => [0, body.bytesize] })
27
113
  padded = header << (" " * ((8 - (header.bytesize % 8)) % 8))
28
- File.binwrite(path, [padded.bytesize].pack("Q<") << padded << body)
114
+ File.open(path, "wb") do |io|
115
+ io.write([padded.bytesize].pack("Q<"))
116
+ io.write(padded)
117
+ io.write(body)
118
+ end
119
+ end
120
+
121
+ def f32_bytes(tensor)
122
+ case tensor.fetch(:dtype)
123
+ when "F32"
124
+ tensor.fetch(:bytes)
125
+ when "F16"
126
+ convert_f16_to_f32(tensor.fetch(:bytes))
127
+ when "BF16"
128
+ convert_bf16_to_f32(tensor.fetch(:bytes))
129
+ else
130
+ raise ConversionError, "tensor dtype #{tensor[:dtype]} is not supported"
131
+ end
29
132
  end
30
133
 
31
- def parse_header(data)
32
- raise ConversionError, "safetensors file is shorter than its length prefix" if data.bytesize < 8
134
+ def read_header(io, file_size)
135
+ raise ConversionError, "safetensors file is shorter than its length prefix" if file_size < 8
136
+
137
+ prefix = io.read(8)
138
+ raise ConversionError, "safetensors file is shorter than its length prefix" unless prefix&.bytesize == 8
33
139
 
34
- header_len = data.byteslice(0, 8).unpack1("Q<")
140
+ header_len = prefix.unpack1("Q<")
35
141
  unless header_len.positive? && header_len <= MAX_HEADER_BYTES
36
142
  raise ConversionError, "implausible safetensors header length #{header_len}"
37
143
  end
38
144
 
39
145
  body_offset = 8 + header_len
40
- raise ConversionError, "safetensors header runs past end of file" if body_offset > data.bytesize
146
+ raise ConversionError, "safetensors header runs past end of file" if body_offset > file_size
41
147
 
42
- raw_header = data.byteslice(8, header_len)
148
+ raw_header = io.read(header_len)
149
+ unless raw_header&.bytesize == header_len
150
+ raise ConversionError, "safetensors header runs past end of file"
151
+ end
43
152
  raise ConversionError, "safetensors header is not a JSON object" unless raw_header.lstrip.start_with?("{")
44
153
 
45
154
  [JSON.parse(raw_header), body_offset]
46
- end
47
-
48
- def tensor_from(data, body_offset, body_size, name, spec)
49
- dtype = spec["dtype"]
50
- shape = spec["shape"]
51
- begin_off, end_off = checked_offsets(name, spec["data_offsets"], body_size)
52
- expected = checked_tensor_bytes(name, dtype, shape)
53
- actual = end_off - begin_off
54
-
55
- if actual != expected
56
- raise ConversionError,
57
- "tensor #{name}: shape #{shape.inspect} implies #{expected} bytes, offsets span #{actual}"
58
- end
59
-
60
- { dtype: dtype, shape: shape, bytes: data.byteslice(body_offset + begin_off, actual) }
155
+ rescue JSON::ParserError => e
156
+ raise ConversionError, "invalid safetensors JSON header: #{e.message}"
61
157
  end
62
158
 
63
159
  def checked_offsets(name, offsets, body_size)
@@ -76,12 +172,60 @@ module StaticEmbeddings
76
172
 
77
173
  def checked_tensor_bytes(name, dtype, shape)
78
174
  element_size = SUPPORTED_DTYPES[dtype]
79
- raise ConversionError, "tensor #{name}: dtype #{dtype} is not supported (F32 only)" unless element_size
175
+ unless element_size
176
+ raise ConversionError, "tensor #{name}: dtype #{dtype} is not supported (expected F32, F16, or BF16)"
177
+ end
80
178
  unless shape.is_a?(Array) && shape.all? { |dim| dim.is_a?(Integer) && dim >= 0 }
81
179
  raise ConversionError, "tensor #{name}: malformed shape #{shape.inspect}"
82
180
  end
83
181
 
84
- shape.reduce(1, :*) * element_size
182
+ shape.reduce(1) { |count, dim| count * dim } * element_size
183
+ end
184
+
185
+ def convert_f16_to_f32(bytes)
186
+ out = String.new(capacity: (bytes.bytesize / 2) * 4, encoding: Encoding::BINARY)
187
+ offset = 0
188
+ chunk_bytes = CONVERT_CHUNK_ELEMENTS * 2
189
+ while offset < bytes.bytesize
190
+ chunk = bytes.byteslice(offset, [chunk_bytes, bytes.bytesize - offset].min)
191
+ floats = if StaticEmbeddings.respond_to?(:decode_f16)
192
+ StaticEmbeddings.decode_f16(chunk)
193
+ else
194
+ chunk.unpack("v*").map { |bits| half_to_float(bits) }
195
+ end
196
+ out << floats.pack("e*")
197
+ offset += chunk.bytesize
198
+ end
199
+ out
200
+ end
201
+
202
+ def convert_bf16_to_f32(bytes)
203
+ out = String.new(capacity: (bytes.bytesize / 2) * 4, encoding: Encoding::BINARY)
204
+ offset = 0
205
+ chunk_bytes = CONVERT_CHUNK_ELEMENTS * 2
206
+ while offset < bytes.bytesize
207
+ chunk = bytes.byteslice(offset, [chunk_bytes, bytes.bytesize - offset].min)
208
+ words = chunk.unpack("v*")
209
+ out << words.map { |bits| bits << 16 }.pack("V*")
210
+ offset += chunk.bytesize
211
+ end
212
+ out
213
+ end
214
+
215
+ def half_to_float(bits)
216
+ sign = (bits >> 15) & 1
217
+ exponent = (bits >> 10) & 0x1F
218
+ fraction = bits & 0x3FF
219
+
220
+ value =
221
+ if exponent.zero?
222
+ fraction.zero? ? 0.0 : Math.ldexp(fraction.to_f, -24)
223
+ elsif exponent == 0x1F
224
+ fraction.zero? ? Float::INFINITY : Float::NAN
225
+ else
226
+ Math.ldexp(1.0 + fraction.to_f / 1024.0, exponent - 15)
227
+ end
228
+ sign.zero? ? value : -value
85
229
  end
86
230
  end
87
231
  end
@@ -1,3 +1,3 @@
1
1
  module StaticEmbeddings
2
- VERSION = "0.1.4"
2
+ VERSION = "0.1.5"
3
3
  end
@@ -18,10 +18,6 @@ end
18
18
  require "static_embeddings/errors"
19
19
  require "static_embeddings/paths"
20
20
  require "static_embeddings/format"
21
- require "static_embeddings/unicode_tables"
22
- require "static_embeddings/safetensors"
23
- require "static_embeddings/converter"
24
- require "static_embeddings/reference"
25
21
  require "static_embeddings/model"
26
22
 
27
23
  module StaticEmbeddings
@@ -64,7 +60,9 @@ module StaticEmbeddings
64
60
  load(path, verify: verify)
65
61
  end
66
62
 
67
- def convert(source_dir, output_path:, model_id: nil, max_tokens: Converter::REFERENCE_MAX_TOKENS)
63
+ def convert(source_dir, output_path:, model_id: nil, max_tokens: nil)
64
+ require "static_embeddings/converter"
65
+ max_tokens = Converter::REFERENCE_MAX_TOKENS if max_tokens.nil?
68
66
  converter = Converter.new(source_dir)
69
67
  converter.convert(output_path: output_path, model_id: model_id, max_tokens: max_tokens)
70
68
  converter.report