static_embeddings 0.1.4 → 1.5.6

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +118 -0
  3. data/README.md +67 -25
  4. data/Rakefile +1 -1
  5. data/docs/ARCHITECTURE.md +54 -34
  6. data/docs/LIMITATIONS.md +12 -6
  7. data/docs/MODEL_AUDIT.md +195 -52
  8. data/ext/static_embeddings/se_embed.c +2 -1
  9. data/ext/static_embeddings/se_format.c +41 -4
  10. data/ext/static_embeddings/se_internal.h +17 -5
  11. data/ext/static_embeddings/se_tokenizer.c +155 -18
  12. data/ext/static_embeddings/se_unicode.c +1 -1
  13. data/ext/static_embeddings/static_embeddings.c +32 -2
  14. data/lib/models/demo.semb +0 -0
  15. data/lib/static_embeddings/bert_wordpiece.rb +191 -0
  16. data/lib/static_embeddings/canonical.rb +50 -0
  17. data/lib/static_embeddings/cli.rb +88 -62
  18. data/lib/static_embeddings/codec.rb +45 -0
  19. data/lib/static_embeddings/conversion.rb +58 -0
  20. data/lib/static_embeddings/errors.rb +2 -2
  21. data/lib/static_embeddings/format/constants.rb +109 -0
  22. data/lib/static_embeddings/format/hash_table.rb +69 -0
  23. data/lib/static_embeddings/format/trie.rb +78 -0
  24. data/lib/static_embeddings/format/verifier.rb +41 -0
  25. data/lib/static_embeddings/format/writer.rb +131 -0
  26. data/lib/static_embeddings/format.rb +3 -300
  27. data/lib/static_embeddings/importers/model2vec.rb +52 -0
  28. data/lib/static_embeddings/importers/sentence_transformers_static.rb +103 -0
  29. data/lib/static_embeddings/importers/support.rb +111 -0
  30. data/lib/static_embeddings/importers.rb +50 -0
  31. data/lib/static_embeddings/model.rb +35 -20
  32. data/lib/static_embeddings/paths.rb +17 -4
  33. data/lib/static_embeddings/provenance.rb +58 -0
  34. data/lib/static_embeddings/reference.rb +90 -33
  35. data/lib/static_embeddings/row_prefix_payload.rb +59 -0
  36. data/lib/static_embeddings/safetensors.rb +178 -34
  37. data/lib/static_embeddings/version.rb +1 -1
  38. data/lib/static_embeddings.rb +29 -57
  39. data/static_embeddings.gemspec +2 -2
  40. data/tools/check_model2vec_parity.rb +89 -54
  41. data/tools/check_st_parity.rb +125 -0
  42. data/tools/eval_retrieval.rb +58 -0
  43. metadata +24 -6
  44. data/lib/static_embeddings/converter.rb +0 -284
@@ -87,16 +87,17 @@ static int push_id(se_scratch_t *sc, size_t *n_ids, uint32_t id) {
87
87
  return 1;
88
88
  }
89
89
 
90
- #define SE_SCRATCH_CPS_KEEP 256
91
- #define SE_SCRATCH_IDS_KEEP 512
92
- #define SE_SCRATCH_BYTES_KEEP 1024
90
+ #define SE_SCRATCH_CPS_KEEP 256
91
+ #define SE_SCRATCH_IDS_RESERVE 512
92
+ #define SE_SCRATCH_IDS_RETAIN_MAX 8192
93
+ #define SE_SCRATCH_BYTES_KEEP 1024
93
94
 
94
95
  int se_scratch_reserve(se_scratch_t *s, uint32_t dim) {
95
96
  if (!grow_u32(&s->cps, &s->cps_cap, SE_SCRATCH_CPS_KEEP))
96
97
  return 0;
97
98
  if (!grow_u32(&s->cps2, &s->cps2_cap, SE_SCRATCH_CPS_KEEP))
98
99
  return 0;
99
- if (!grow_u32(&s->ids, &s->ids_cap, SE_SCRATCH_IDS_KEEP))
100
+ if (!grow_u32(&s->ids, &s->ids_cap, SE_SCRATCH_IDS_RESERVE))
100
101
  return 0;
101
102
  if (!grow_bytes(&s->bytes, &s->bytes_cap, SE_SCRATCH_BYTES_KEEP))
102
103
  return 0;
@@ -149,7 +150,8 @@ static int shrink_bytes(uint8_t **buf, size_t *cap, size_t keep) {
149
150
  static void se_scratch_trim(se_scratch_t *s) {
150
151
  (void)shrink_u32(&s->cps, &s->cps_cap, SE_SCRATCH_CPS_KEEP);
151
152
  (void)shrink_u32(&s->cps2, &s->cps2_cap, SE_SCRATCH_CPS_KEEP);
152
- (void)shrink_u32(&s->ids, &s->ids_cap, SE_SCRATCH_IDS_KEEP);
153
+ if (s->ids_cap > SE_SCRATCH_IDS_RETAIN_MAX)
154
+ (void)shrink_u32(&s->ids, &s->ids_cap, SE_SCRATCH_IDS_RETAIN_MAX);
153
155
  (void)shrink_bytes(&s->bytes, &s->bytes_cap, SE_SCRATCH_BYTES_KEEP);
154
156
  }
155
157
 
@@ -393,6 +395,37 @@ static int cp_is_cjk_segment(const se_model_t *m, uint32_t cp) {
393
395
  return m->meta.handle_chinese_chars && cp >= 0x3400 && se_is_cjk(cp);
394
396
  }
395
397
 
398
+ typedef struct {
399
+ const char *text;
400
+ uint32_t len;
401
+ uint32_t bit;
402
+ } se_added_token_spec_t;
403
+
404
+ static const se_added_token_spec_t SE_ADDED_TOKENS[] = {
405
+ {"[MASK]", 6u, SE_ADDED_MASK}, {"[PAD]", 5u, SE_ADDED_PAD}, {"[UNK]", 5u, SE_ADDED_UNK},
406
+ {"[CLS]", 5u, SE_ADDED_CLS}, {"[SEP]", 5u, SE_ADDED_SEP},
407
+ };
408
+
409
+ static int boundary_splits_added_token(const se_model_t *model, const uint8_t *input,
410
+ size_t input_len, size_t boundary) {
411
+ if (model->meta.added_token_mask == 0 || boundary == 0 || boundary >= input_len)
412
+ return 0;
413
+
414
+ for (size_t k = 0; k < SE_ARRAY_LEN(SE_ADDED_TOKENS); k++) {
415
+ const se_added_token_spec_t *token = &SE_ADDED_TOKENS[k];
416
+ if ((model->meta.added_token_mask & token->bit) == 0)
417
+ continue;
418
+ for (size_t back = 1; back < token->len && back <= boundary; back++) {
419
+ size_t start = boundary - back;
420
+ if (start + token->len > input_len || input[start] != '[')
421
+ continue;
422
+ if (memcmp(input + start, token->text, token->len) == 0)
423
+ return 1;
424
+ }
425
+ }
426
+ return 0;
427
+ }
428
+
396
429
  size_t se_prefix_boundary_len(const se_model_t *model, const uint8_t *input, size_t input_len,
397
430
  size_t target, size_t backscan) {
398
431
  if (target >= input_len)
@@ -420,14 +453,15 @@ size_t se_prefix_boundary_len(const se_model_t *model, const uint8_t *input, siz
420
453
  continue;
421
454
 
422
455
  if (cp_is_cjk_segment(model, cp)) {
423
- if (after <= target)
456
+ if (after <= target && !boundary_splits_added_token(model, input, input_len, after))
424
457
  return after;
425
- if (pos > 0)
458
+ if (pos > 0 && !boundary_splits_added_token(model, input, input_len, pos))
426
459
  return pos;
427
- return 0;
460
+ continue;
428
461
  }
429
462
 
430
- if ((is_whitespace(model, cp) || is_punct(model, cp)) && after <= target)
463
+ if ((is_whitespace(model, cp) || is_punct(model, cp)) && after <= target &&
464
+ !boundary_splits_added_token(model, input, input_len, after))
431
465
  return after;
432
466
  }
433
467
 
@@ -454,6 +488,7 @@ typedef struct {
454
488
  const se_model_t *model;
455
489
  se_scratch_t *scratch;
456
490
  uint32_t max_tokens;
491
+ se_token_limit_t limit_mode;
457
492
  size_t n_ids;
458
493
  size_t n_unk;
459
494
  size_t segment_len;
@@ -481,17 +516,44 @@ static int append_segment_cp(token_state_t *st, uint32_t cp) {
481
516
  return 1;
482
517
  }
483
518
 
519
+ static size_t limited_token_count(const token_state_t *st) {
520
+ if (st->limit_mode == SE_TOKEN_LIMIT_USABLE && st->model->meta.unk_policy == SE_UNK_DROP)
521
+ return st->n_ids - st->n_unk;
522
+ return st->n_ids;
523
+ }
524
+
484
525
  static int cap_after_append(token_state_t *st) {
485
- if (st->max_tokens == 0 || st->n_ids <= (size_t)st->max_tokens)
526
+ if (st->max_tokens == 0 || limited_token_count(st) <= (size_t)st->max_tokens)
486
527
  return 0;
487
528
 
488
- size_t dropped_unk = 0;
489
- for (size_t k = st->max_tokens; k < st->n_ids; k++) {
490
- if (st->scratch->ids[k] == st->model->meta.unk_id)
491
- dropped_unk++;
529
+ if (st->limit_mode == SE_TOKEN_LIMIT_RAW || st->model->meta.unk_policy != SE_UNK_DROP) {
530
+ size_t dropped_unk = 0;
531
+ for (size_t k = st->max_tokens; k < st->n_ids; k++) {
532
+ if (st->scratch->ids[k] == st->model->meta.unk_id)
533
+ dropped_unk++;
534
+ }
535
+ st->n_unk -= dropped_unk;
536
+ st->n_ids = st->max_tokens;
537
+ } else {
538
+ size_t usable = 0;
539
+ size_t kept_unk = 0;
540
+ size_t keep = 0;
541
+ for (size_t k = 0; k < st->n_ids; k++) {
542
+ uint32_t id = st->scratch->ids[k];
543
+ if (id == st->model->meta.unk_id) {
544
+ kept_unk++;
545
+ continue;
546
+ }
547
+ usable++;
548
+ if (usable == (size_t)st->max_tokens) {
549
+ keep = k + 1;
550
+ break;
551
+ }
552
+ }
553
+ st->n_ids = keep;
554
+ st->n_unk = kept_unk;
492
555
  }
493
- st->n_unk -= dropped_unk;
494
- st->n_ids = st->max_tokens;
556
+
495
557
  st->stats->truncated = 1;
496
558
  return 1;
497
559
  }
@@ -602,6 +664,54 @@ static wordpiece_status_t wordpiece(const se_model_t *m, se_scratch_t *sc, const
602
664
  return 1;
603
665
  }
604
666
 
667
+ static uint32_t added_token_id(const se_model_t *model, uint32_t bit) {
668
+ switch (bit) {
669
+ case SE_ADDED_PAD:
670
+ return model->meta.pad_id;
671
+ case SE_ADDED_UNK:
672
+ return model->meta.unk_id;
673
+ case SE_ADDED_CLS:
674
+ return model->meta.cls_id;
675
+ case SE_ADDED_SEP:
676
+ return model->meta.sep_id;
677
+ case SE_ADDED_MASK:
678
+ return model->meta.mask_id;
679
+ default:
680
+ return SE_SLOT_EMPTY;
681
+ }
682
+ }
683
+
684
+ static int match_added_token(const se_model_t *model, const uint8_t *input, size_t input_len,
685
+ size_t pos, uint32_t *id_out, size_t *len_out) {
686
+ if (model->meta.added_token_mask == 0 || pos >= input_len || input[pos] != '[')
687
+ return 0;
688
+
689
+ for (size_t k = 0; k < SE_ARRAY_LEN(SE_ADDED_TOKENS); k++) {
690
+ const se_added_token_spec_t *token = &SE_ADDED_TOKENS[k];
691
+ if ((model->meta.added_token_mask & token->bit) == 0)
692
+ continue;
693
+ if (token->len > input_len - pos)
694
+ continue;
695
+ if (memcmp(input + pos, token->text, token->len) != 0)
696
+ continue;
697
+
698
+ *id_out = added_token_id(model, token->bit);
699
+ *len_out = token->len;
700
+ return 1;
701
+ }
702
+ return 0;
703
+ }
704
+
705
+ static se_status_t append_direct_id(token_state_t *st, uint32_t id, int *stop) {
706
+ if (!push_id(st->scratch, &st->n_ids, id))
707
+ return oom(st, "adding a special token");
708
+ if (id == st->model->meta.unk_id)
709
+ st->n_unk++;
710
+ if (cap_after_append(st))
711
+ *stop = 1;
712
+ return SE_OK;
713
+ }
714
+
605
715
  static se_status_t append_wordpiece(token_state_t *st, const uint32_t *word, size_t word_len,
606
716
  int *stop) {
607
717
  wordpiece_status_t wp =
@@ -752,6 +862,31 @@ static se_status_t tokenize_ascii_run(token_state_t *st, const uint8_t *input, s
752
862
  if (b >= 0x80)
753
863
  break;
754
864
 
865
+ if (b == '[' && st->model->meta.added_token_mask != 0) {
866
+ uint32_t added_id = 0;
867
+ size_t added_len = 0;
868
+ if (match_added_token(st->model, input, input_len, i, &added_id, &added_len)) {
869
+ se_status_t rc = flush_segment(st, stop);
870
+ if (rc != SE_OK) {
871
+ *ip = i;
872
+ return rc;
873
+ }
874
+ if (*stop) {
875
+ *ip = i;
876
+ return SE_OK;
877
+ }
878
+ rc = append_direct_id(st, added_id, stop);
879
+ if (rc != SE_OK) {
880
+ *ip = i;
881
+ return rc;
882
+ }
883
+ i += added_len;
884
+ if (*stop)
885
+ break;
886
+ continue;
887
+ }
888
+ }
889
+
755
890
  if (i >= next_cancel_check) {
756
891
  if (token_cancelled(st)) {
757
892
  *ip = i;
@@ -811,8 +946,9 @@ static se_status_t tokenize_ascii_run(token_state_t *st, const uint8_t *input, s
811
946
  }
812
947
 
813
948
  se_status_t se_tokenize(const se_model_t *model, se_scratch_t *sc, const uint8_t *input,
814
- size_t input_len, uint32_t max_tokens, se_token_stats_t *stats,
815
- se_error_t *err, volatile sig_atomic_t *cancelled) {
949
+ size_t input_len, uint32_t max_tokens, se_token_limit_t limit_mode,
950
+ se_token_stats_t *stats, se_error_t *err,
951
+ volatile sig_atomic_t *cancelled) {
816
952
  memset(stats, 0, sizeof(*stats));
817
953
 
818
954
  token_state_t st;
@@ -820,6 +956,7 @@ se_status_t se_tokenize(const se_model_t *model, se_scratch_t *sc, const uint8_t
820
956
  st.model = model;
821
957
  st.scratch = sc;
822
958
  st.max_tokens = max_tokens;
959
+ st.limit_mode = limit_mode;
823
960
  st.stats = stats;
824
961
  st.err = err;
825
962
  st.cancelled = cancelled;
@@ -115,6 +115,6 @@ const se_map_entry_t *se_map_lookup(const se_map_entry_t *entries, uint32_t coun
115
115
  int se_is_cjk(uint32_t cp) {
116
116
  return (cp >= 0x4E00 && cp <= 0x9FFF) || (cp >= 0x3400 && cp <= 0x4DBF) ||
117
117
  (cp >= 0x20000 && cp <= 0x2A6DF) || (cp >= 0x2A700 && cp <= 0x2B73F) ||
118
- (cp >= 0x2B740 && cp <= 0x2B81F) || (cp >= 0x2B820 && cp <= 0x2CEAF) ||
118
+ (cp >= 0x2B740 && cp <= 0x2B81F) || (cp >= 0x2B920 && cp <= 0x2CEAF) ||
119
119
  (cp >= 0xF900 && cp <= 0xFAFF) || (cp >= 0x2F800 && cp <= 0x2FA1F);
120
120
  }
@@ -80,6 +80,7 @@ static ID id_blocking_p;
80
80
  static ID id_vector;
81
81
  static ID id_token_count;
82
82
  static ID id_unk_count;
83
+ static ID id_pooled_count;
83
84
  static ID id_truncated;
84
85
  static ID id_dim;
85
86
  static ID id_allow_unfrozen;
@@ -1030,6 +1031,10 @@ static VALUE model_embed_with_stats(int argc, VALUE *argv, VALUE self) {
1030
1031
  rb_hash_aset(hash, ID2SYM(id_vector), vector);
1031
1032
  rb_hash_aset(hash, ID2SYM(id_token_count), UINT2NUM(stats.token_count));
1032
1033
  rb_hash_aset(hash, ID2SYM(id_unk_count), UINT2NUM(stats.unk_count));
1034
+ uint32_t pooled_count = get_model(self)->model.meta.unk_policy == SE_UNK_DROP
1035
+ ? stats.token_count - stats.unk_count
1036
+ : stats.token_count;
1037
+ rb_hash_aset(hash, ID2SYM(id_pooled_count), UINT2NUM(pooled_count));
1033
1038
  rb_hash_aset(hash, ID2SYM(id_truncated), stats.truncated ? Qtrue : Qfalse);
1034
1039
  return hash;
1035
1040
  }
@@ -1049,7 +1054,7 @@ typedef struct {
1049
1054
  static VALUE tokenize_scratch_body(VALUE arg) {
1050
1055
  tokenize_scratch_job_t *job = (tokenize_scratch_job_t *)(uintptr_t)arg;
1051
1056
  job->rc = se_tokenize(job->model, job->scratch, job->input, job->input_len, job->max_tokens,
1052
- &job->stats, &job->err, NULL);
1057
+ SE_TOKEN_LIMIT_RAW, &job->stats, &job->err, NULL);
1053
1058
  if (job->rc != SE_OK)
1054
1059
  return Qnil;
1055
1060
 
@@ -1237,7 +1242,27 @@ static VALUE embed_token_ids_value(VALUE self, VALUE ids_value, VALUE max_tokens
1237
1242
  size_t n = (size_t)n_long;
1238
1243
  int truncated = 0;
1239
1244
 
1240
- if (max_tokens != 0 && n > (size_t)max_tokens) {
1245
+ if (max_tokens != 0 && w->model.meta.unk_policy == SE_UNK_DROP) {
1246
+ size_t usable = 0;
1247
+ size_t cutoff = n;
1248
+ for (size_t i = 0; i < n; i++) {
1249
+ unsigned long long value = NUM2ULL(rb_ary_entry(ids_value, (long)i));
1250
+ if (value >= w->model.meta.vocab_size)
1251
+ rb_raise(rb_eArgError, "token id at index %zu is out of range", i);
1252
+
1253
+ if ((uint32_t)value == w->model.meta.unk_id)
1254
+ continue;
1255
+
1256
+ usable++;
1257
+ if (usable == (size_t)max_tokens) {
1258
+ cutoff = i + 1;
1259
+ } else if (usable > (size_t)max_tokens) {
1260
+ n = cutoff;
1261
+ truncated = 1;
1262
+ break;
1263
+ }
1264
+ }
1265
+ } else if (max_tokens != 0 && n > (size_t)max_tokens) {
1241
1266
  n = (size_t)max_tokens;
1242
1267
  truncated = 1;
1243
1268
  }
@@ -1297,6 +1322,10 @@ static VALUE model_embed_token_ids_with_stats(int argc, VALUE *argv, VALUE self)
1297
1322
  rb_hash_aset(hash, ID2SYM(id_vector), vector);
1298
1323
  rb_hash_aset(hash, ID2SYM(id_token_count), UINT2NUM(stats.token_count));
1299
1324
  rb_hash_aset(hash, ID2SYM(id_unk_count), UINT2NUM(stats.unk_count));
1325
+ uint32_t pooled_count = get_model(self)->model.meta.unk_policy == SE_UNK_DROP
1326
+ ? stats.token_count - stats.unk_count
1327
+ : stats.token_count;
1328
+ rb_hash_aset(hash, ID2SYM(id_pooled_count), UINT2NUM(pooled_count));
1300
1329
  rb_hash_aset(hash, ID2SYM(id_truncated), stats.truncated ? Qtrue : Qfalse);
1301
1330
  return hash;
1302
1331
  }
@@ -1607,6 +1636,7 @@ RUBY_FUNC_EXPORTED void Init_static_embeddings(void) {
1607
1636
  id_vector = rb_intern("vector");
1608
1637
  id_token_count = rb_intern("token_count");
1609
1638
  id_unk_count = rb_intern("unk_count");
1639
+ id_pooled_count = rb_intern("pooled_count");
1610
1640
  id_truncated = rb_intern("truncated");
1611
1641
  id_dim = rb_intern("dim");
1612
1642
  id_allow_unfrozen = rb_intern("allow_unfrozen");
Binary file
@@ -0,0 +1,191 @@
1
+ require "static_embeddings/errors"
2
+ require "static_embeddings/format/constants"
3
+
4
+ module StaticEmbeddings
5
+ module BertWordPiece
6
+ TOKENIZER_PROFILE = "BERT_WORDPIECE_V1"
7
+ ALLOWED_NORMALIZER_KEYS = %w[type clean_text handle_chinese_chars strip_accents lowercase].freeze
8
+ STANDARD_SPECIAL_TOKENS = {
9
+ "[PAD]" => Format::ADDED_PAD,
10
+ "[UNK]" => Format::ADDED_UNK,
11
+ "[CLS]" => Format::ADDED_CLS,
12
+ "[SEP]" => Format::ADDED_SEP,
13
+ "[MASK]" => Format::ADDED_MASK
14
+ }.freeze
15
+
16
+ module_function
17
+
18
+ def compile(tokenizer, tokenizer_config = {})
19
+ profile = audit(tokenizer, tokenizer_config)
20
+ tokens = vocabulary(tokenizer)
21
+ validate_added_token_ids(profile.fetch(:added_tokens), tokens)
22
+ [profile.freeze, tokens]
23
+ end
24
+
25
+ def runtime_meta(profile, tokens)
26
+ prefix = profile.fetch(:continuing_subword_prefix)
27
+ {
28
+ tokenizer_type: Format::TOKENIZER_BERT_WORDPIECE_V1,
29
+ tokenizer_profile: TOKENIZER_PROFILE,
30
+ do_lower_case: profile.fetch(:lowercase),
31
+ strip_accents: profile.fetch(:strip_accents),
32
+ handle_chinese_chars: profile.fetch(:handle_chinese_chars),
33
+ clean_text: profile.fetch(:clean_text),
34
+ added_token_mask: profile.fetch(:added_token_mask),
35
+ max_input_chars_per_word: profile.fetch(:max_input_chars_per_word),
36
+ max_token_chars: max_token_chars(tokens, prefix),
37
+ subword_prefix: prefix,
38
+ pad_id: token_id(tokens, "[PAD]", 0),
39
+ unk_id: token_id(tokens, profile.fetch(:unk_token)),
40
+ cls_id: token_id(tokens, "[CLS]", 0),
41
+ sep_id: token_id(tokens, "[SEP]", 0),
42
+ mask_id: token_id(tokens, "[MASK]", 0)
43
+ }.freeze
44
+ end
45
+
46
+ def audit(tokenizer, tokenizer_config)
47
+ unsupported!("tokenizer.json is not an object") unless tokenizer.is_a?(Hash)
48
+ unsupported!("tokenizer_config.json is not an object") unless tokenizer_config.is_a?(Hash)
49
+ model = tokenizer["model"] || unsupported!("tokenizer.json has no model section")
50
+ normalizer = tokenizer["normalizer"] || unsupported!("tokenizer has no normalizer")
51
+ pre_tokenizer = tokenizer["pre_tokenizer"]
52
+ unsupported!("tokenizer model section is not an object") unless model.is_a?(Hash)
53
+ unsupported!("tokenizer normalizer is not an object") unless normalizer.is_a?(Hash)
54
+
55
+ audit_model(model)
56
+ audit_normalizer(normalizer)
57
+ audit_pre_tokenizer(pre_tokenizer)
58
+ added_tokens = audit_added_tokens(tokenizer["added_tokens"] || [])
59
+ lowercase = flag(normalizer, "lowercase", tokenizer_config["do_lower_case"], true)
60
+ clean_text = flag(normalizer, "clean_text", nil, true)
61
+ max_input_chars = Integer(model.fetch("max_input_chars_per_word", 100))
62
+ unsupported!("max_input_chars_per_word must be positive") unless max_input_chars.positive?
63
+ unsupported!("clean_text=false is not supported by the runtime") unless clean_text
64
+
65
+ {
66
+ lowercase: lowercase,
67
+ strip_accents: strip_accents(normalizer, lowercase),
68
+ clean_text: clean_text,
69
+ handle_chinese_chars: flag(normalizer, "handle_chinese_chars",
70
+ tokenizer_config["tokenize_chinese_chars"], true),
71
+ continuing_subword_prefix: model.fetch("continuing_subword_prefix", "##"),
72
+ unk_token: model.fetch("unk_token", "[UNK]"),
73
+ max_input_chars_per_word: max_input_chars,
74
+ tokenizer_class: tokenizer_config["tokenizer_class"],
75
+ added_tokens: added_tokens,
76
+ added_token_mask: added_tokens.reduce(0) do |mask, token|
77
+ mask | STANDARD_SPECIAL_TOKENS.fetch(token.fetch("content"))
78
+ end
79
+ }
80
+ end
81
+
82
+ def audit_model(model)
83
+ unsupported!("tokenizer model.type is #{model['type'].inspect}, expected WordPiece") unless model["type"] == "WordPiece"
84
+
85
+ prefix = model.fetch("continuing_subword_prefix", "##")
86
+ unsupported!("continuing_subword_prefix #{prefix.inspect} is not supported") unless prefix == "##"
87
+
88
+ unk_token = model.fetch("unk_token", "[UNK]")
89
+ unsupported!("unk_token #{unk_token.inspect} is not supported") unless unk_token == "[UNK]"
90
+ end
91
+
92
+ def audit_normalizer(normalizer)
93
+ unsupported!("normalizer type #{normalizer['type'].inspect} is not BertNormalizer") unless normalizer["type"] == "BertNormalizer"
94
+
95
+ unknown = normalizer.keys - ALLOWED_NORMALIZER_KEYS
96
+ unsupported!("normalizer has unsupported keys #{unknown.inspect}") unless unknown.empty?
97
+ end
98
+
99
+ def audit_pre_tokenizer(pre_tokenizer)
100
+ return if pre_tokenizer.is_a?(Hash) && pre_tokenizer["type"] == "BertPreTokenizer"
101
+
102
+ unsupported!("pre_tokenizer type #{pre_tokenizer && pre_tokenizer['type'].inspect} is not BertPreTokenizer")
103
+ end
104
+
105
+ def audit_added_tokens(tokens)
106
+ unsupported!("added_tokens is not an array") unless tokens.is_a?(Array)
107
+ unsupported!("added_tokens contains a non-object entry") unless tokens.all? { |token| token.is_a?(Hash) }
108
+ bad = tokens.reject { |token| standard_special?(token) }
109
+ unsupported!("tokenizer declares non-standard added_tokens #{bad.map { |token| token['content'] }.inspect}") unless bad.empty?
110
+
111
+ tokens.each do |token|
112
+ content = token["content"].to_s
113
+ unsupported!("added token #{content.inspect} contains whitespace") if content.match?(/\s/)
114
+ unsupported!("added token #{content.inspect} uses lstrip/rstrip/single_word") if token["lstrip"] || token["rstrip"] || token["single_word"]
115
+ unsupported!("added token #{content.inspect} must use normalized=false") unless token["normalized"] == false
116
+ end
117
+
118
+ duplicate = tokens.group_by { |token| token["content"] }.find { |_, rows| rows.length > 1 }
119
+ unsupported!("duplicate added token #{duplicate.first.inspect}") if duplicate
120
+ tokens.freeze
121
+ end
122
+
123
+ def vocabulary(tokenizer)
124
+ vocab = tokenizer.dig("model", "vocab") || unsupported!("tokenizer.json has no model.vocab")
125
+ unsupported!("tokenizer model.vocab is not an object") unless vocab.is_a?(Hash)
126
+ tokens = Array.new(vocab.length)
127
+ vocab.each { |token, id| assign_vocab(tokens, token, id) }
128
+ missing = tokens.index(nil)
129
+ invalid!("vocab has a hole at id #{missing}") if missing
130
+ tokens
131
+ end
132
+
133
+ def assign_vocab(tokens, token, id)
134
+ unsupported!("vocab id #{id.inspect} is not an integer") unless id.is_a?(Integer)
135
+ invalid!("vocab id #{id} for #{token.inspect} is outside 0...#{tokens.length}") unless id.between?(0, tokens.length - 1)
136
+ invalid!("duplicate vocab id #{id}") unless tokens[id].nil?
137
+ tokens[id] = token
138
+ end
139
+
140
+ def validate_added_token_ids(added_tokens, tokens)
141
+ added_tokens.each do |token|
142
+ content = token.fetch("content")
143
+ id = token["id"]
144
+ unsupported!("added token #{content.inspect} has non-integer id #{id.inspect}") unless id.is_a?(Integer)
145
+ valid = id.between?(0, tokens.length - 1) && tokens[id] == content
146
+ unsupported!("added token #{content.inspect} id #{id} does not match model.vocab") unless valid
147
+ end
148
+ end
149
+
150
+ def flag(hash, key, fallback, default)
151
+ return !!hash[key] if hash.key?(key) && !hash[key].nil?
152
+ return !!fallback unless fallback.nil?
153
+
154
+ default
155
+ end
156
+
157
+ def strip_accents(normalizer, lowercase)
158
+ value = normalizer["strip_accents"]
159
+ normalizer.key?("strip_accents") && !value.nil? ? !!value : lowercase
160
+ end
161
+
162
+ def standard_special?(token)
163
+ token["special"] && STANDARD_SPECIAL_TOKENS.key?(token["content"])
164
+ end
165
+
166
+ def max_token_chars(tokens, prefix)
167
+ tokens.reduce(1) do |maximum, token|
168
+ body = token.start_with?(prefix) ? token[prefix.length..] : token
169
+ [maximum, body.each_char.count].max
170
+ end
171
+ end
172
+
173
+ def token_id(tokens, token, fallback = nil)
174
+ id = tokens.index(token)
175
+ return id unless id.nil?
176
+ return fallback unless fallback.nil?
177
+
178
+ invalid!("vocabulary has no #{token.inspect}")
179
+ end
180
+
181
+ def unsupported!(message)
182
+ raise UnsupportedModelError,
183
+ "#{message}. Supported tokenizer profile: #{TOKENIZER_PROFILE}; " \
184
+ "other tokenizer behaviour requires a separate runtime capability."
185
+ end
186
+
187
+ def invalid!(message)
188
+ raise ConversionError, message
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,50 @@
1
+ module StaticEmbeddings
2
+ module Canonical
3
+ Dimensions = Struct.new(:native, :output, :trained, keyword_init: true)
4
+ Runtime = Struct.new(:normalization, :unk_policy, :empty_policy, :max_tokens, :add_special_tokens,
5
+ keyword_init: true)
6
+ Source = Struct.new(:family, :model, :revision, :oracle, :files_sha256, :tokenizer_class,
7
+ :config_seq_length, keyword_init: true)
8
+ Model = Struct.new(:tokens, :matrix, :dimensions, :runtime, :tokenizer, :source, keyword_init: true)
9
+
10
+ module_function
11
+
12
+ def dimensions(native:, output:, trained: nil)
13
+ Dimensions.new(native: native, output: output, trained: trained&.freeze).freeze
14
+ end
15
+
16
+ def runtime(normalization:, unk_policy:, empty_policy:, max_tokens:, add_special_tokens: false)
17
+ Runtime.new(
18
+ normalization: normalization,
19
+ unk_policy: unk_policy,
20
+ empty_policy: empty_policy,
21
+ max_tokens: max_tokens,
22
+ add_special_tokens: add_special_tokens
23
+ ).freeze
24
+ end
25
+
26
+ def source(family:, model:, oracle:, files_sha256:, revision: nil, tokenizer_class: nil,
27
+ config_seq_length: nil)
28
+ Source.new(
29
+ family: family,
30
+ model: model,
31
+ revision: revision,
32
+ oracle: oracle,
33
+ files_sha256: files_sha256.freeze,
34
+ tokenizer_class: tokenizer_class,
35
+ config_seq_length: config_seq_length
36
+ ).freeze
37
+ end
38
+
39
+ def model(tokens:, matrix:, dimensions:, runtime:, tokenizer:, source:)
40
+ Model.new(
41
+ tokens: tokens.freeze,
42
+ matrix: matrix,
43
+ dimensions: dimensions,
44
+ runtime: runtime,
45
+ tokenizer: tokenizer.freeze,
46
+ source: source
47
+ ).freeze
48
+ end
49
+ end
50
+ end