@@ -21,6 +21,10 @@ def initialize(graph:, classes:, k_passes: 2)
2121 @n_classes = classes . length
2222 @mask_id = @n_classes
2323 @k_passes = k_passes
24+ # a character is a diacritic iff it occurs inside some class the
25+ # artifact knows — no per-language mark tables needed
26+ @mark_chars = classes . flat_map { |c | c . chars } . reject ( &:empty? ) . uniq . freeze
27+ @class_to_id = classes . each_with_index . to_h . freeze
2428 # the onnxruntime gem loads from paths, not bytes (as byt5_onnx):
2529 # verified bytes go to a tmpfile owned by this instance
2630 @graph_tmpdir = Dir . mktmpdir ( 'isx-plane' )
@@ -29,17 +33,69 @@ def initialize(graph:, classes:, k_passes: 2)
2933 @sess = OnnxRuntime ::Model . new ( path )
3034 end
3135
32- def translate ( text )
33- ids = IMF . encode ( text ) [ 0 ..-2 ] # strip EOS: one token per byte
34- n = ids . length
35- plane = Array . new ( n , @mask_id )
36- preds = nil
37- @k_passes . times do
38- logits = @sess . predict ( { 'input_ids' => [ ids ] , 'plane_ids' => [ plane ] } ) [ 'class_logits' ] [ 0 ]
39- preds = logits . each_with_index . map { |row , i | row . index ( row . max ) }
40- plane = preds
36+ # Split text into (skeleton, per-base classes). Marks FOLLOW their
37+ # base letter; leading marks bind to a "\x00" anchor (render drops
38+ # it). Cluster order is preserved as written — render-exactness
39+ # beats normalization (the Hebrew canon lesson).
40+ def split_planes ( text )
41+ skeleton = [ ]
42+ classes = [ ]
43+ current = [ ]
44+ close = lambda do
45+ next if current . empty?
46+
47+ if skeleton . empty?
48+ skeleton << "\x00 "
49+ classes << current . join
50+ else
51+ classes [ -1 ] = classes [ -1 ] + current . join
52+ end
53+ current = [ ]
54+ end
55+ text . each_char do |ch |
56+ if @mark_chars . include? ( ch )
57+ current << ch
58+ else
59+ close . call
60+ skeleton << ch
61+ classes << ''
62+ end
63+ end
64+ close . call
65+ [ skeleton . join , classes ]
66+ end
67+
68+ def translate ( text , preserve_diacritics : false )
69+ return translate_plain ( text ) unless preserve_diacritics
70+
71+ skeleton , char_classes = split_planes ( text )
72+ tok_preds = decode_pinned ( skeleton , char_classes )
73+ pos = 0
74+ final = char_classes . each_with_index . map do |user_cls , i |
75+ ch = skeleton [ i ]
76+ n = ch . bytesize
77+ cls = if user_cls . empty?
78+ votes = tok_preds [ pos , n ] || [ ]
79+ cid = votes . tally . max_by { |_ , c | c } &.first || @n_classes
80+ cid < @n_classes ? @classes [ cid ] : ''
81+ else
82+ user_cls # user diacritics round-trip byte-exactly
83+ end
84+ pos += n
85+ cls
86+ end
87+ out = +""
88+ skeleton . each_char . with_index do |ch , i |
89+ out << ( ch == "\x00 " ? final [ i ] : ch + final [ i ] )
4190 end
91+ out
92+ end
93+
94+ private
4295
96+ def translate_plain ( text )
97+ ids = IMF . encode ( text ) [ 0 ..-2 ] # strip EOS: one token per byte
98+ preds = k_pass_decode ( ids , Array . new ( ids . length , @mask_id ) )
4399 pos = 0
44100 char_preds = text . each_char . map do |ch |
45101 nbytes = ch . bytesize
@@ -51,6 +107,32 @@ def translate(text)
51107 PlaneModel . render_plane ( text , char_preds )
52108 end
53109
110+ def k_pass_decode ( ids , plane0 )
111+ plane = plane0
112+ preds = nil
113+ @k_passes . times do
114+ logits = @sess . predict ( { 'input_ids' => [ ids ] , 'plane_ids' => [ plane ] } ) [ 'class_logits' ] [ 0 ]
115+ preds = logits . each_with_index . map { |row , i | row . index ( row . max ) }
116+ plane = preds
117+ end
118+ preds
119+ end
120+
121+ # K-pass decode with user classes pinned: known classes are
122+ # conditioned AND never overwritten; unknown non-empty clusters
123+ # stay masked during decode but are restored at render.
124+ def decode_pinned ( skeleton , char_classes )
125+ ids = IMF . encode ( skeleton ) [ 0 ..-2 ]
126+ per_token_pin = [ ]
127+ skeleton . each_char . with_index do |ch , i |
128+ cls = char_classes [ i ]
129+ pid = @class_to_id . fetch ( cls , @mask_id )
130+ ch . bytesize . times { per_token_pin << pid }
131+ end
132+ plane0 = per_token_pin . map { |p | p == @mask_id ? @mask_id : p }
133+ k_pass_decode ( ids , plane0 )
134+ end
135+
54136 def self . render_plane ( skeleton , char_classes )
55137 skeleton . each_char . with_index . map do |ch , i |
56138 ch + char_classes [ i ] . to_s
0 commit comments