interscript 2.4.5 → 3.0.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 (120) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +15 -0
  3. data/docs/demo/20191118-interscript-demo-cast.gif +0 -0
  4. data/exe/codemod-imp-to-isc +5 -0
  5. data/exe/diagnose_parse_failures +47 -0
  6. data/exe/interscript +2 -1
  7. data/exe/verify_isc_deep +211 -0
  8. data/exe/verify_isc_equivalence +128 -0
  9. data/interscript.gemspec +27 -20
  10. data/lib/interscript/command.rb +21 -12
  11. data/lib/interscript/compiler/javascript.rb +34 -35
  12. data/lib/interscript/compiler/json_ir.rb +214 -0
  13. data/lib/interscript/compiler/python.rb +354 -0
  14. data/lib/interscript/compiler/ruby.rb +23 -22
  15. data/lib/interscript/compiler.rb +20 -3
  16. data/lib/interscript/detector.rb +14 -7
  17. data/lib/interscript/dsl/aliases.rb +1 -1
  18. data/lib/interscript/dsl/document.rb +6 -3
  19. data/lib/interscript/dsl/group/parallel.rb +1 -1
  20. data/lib/interscript/dsl/group.rb +18 -9
  21. data/lib/interscript/dsl/items.rb +28 -16
  22. data/lib/interscript/dsl/metadata.rb +19 -18
  23. data/lib/interscript/dsl/stage.rb +1 -1
  24. data/lib/interscript/dsl/symbol_mm.rb +4 -2
  25. data/lib/interscript/dsl/tests.rb +1 -1
  26. data/lib/interscript/dsl.rb +21 -13
  27. data/lib/interscript/interpreter.rb +32 -21
  28. data/lib/interscript/isc/codemod.rb +791 -0
  29. data/lib/interscript/isc/document_builder.rb +354 -0
  30. data/lib/interscript/isc/generator.rb +191 -0
  31. data/lib/interscript/isc/grammar/concerns/aliases.rb +29 -0
  32. data/lib/interscript/isc/grammar/concerns/dependencies.rb +23 -0
  33. data/lib/interscript/isc/grammar/concerns/items.rb +176 -0
  34. data/lib/interscript/isc/grammar/concerns/metadata.rb +142 -0
  35. data/lib/interscript/isc/grammar/concerns/primitives.rb +112 -0
  36. data/lib/interscript/isc/grammar/concerns/stages.rb +129 -0
  37. data/lib/interscript/isc/grammar/concerns/system.rb +37 -0
  38. data/lib/interscript/isc/grammar/concerns/tests.rb +31 -0
  39. data/lib/interscript/isc/grammar/concerns.rb +18 -0
  40. data/lib/interscript/isc/grammar/core.rb +23 -0
  41. data/lib/interscript/isc/grammar/isc.artifact.json +1 -0
  42. data/lib/interscript/isc/grammar/isc.parg +196 -0
  43. data/lib/interscript/isc/grammar.rb +12 -0
  44. data/lib/interscript/isc/items.rb +185 -0
  45. data/lib/interscript/isc/model/alias.rb +20 -0
  46. data/lib/interscript/isc/model/constraint.rb +20 -0
  47. data/lib/interscript/isc/model/dependency.rb +19 -0
  48. data/lib/interscript/isc/model/document.rb +31 -0
  49. data/lib/interscript/isc/model/item.rb +50 -0
  50. data/lib/interscript/isc/model/rule.rb +23 -0
  51. data/lib/interscript/isc/model/stage.rb +20 -0
  52. data/lib/interscript/isc/model/stage_item.rb +33 -0
  53. data/lib/interscript/isc/model/test.rb +21 -0
  54. data/lib/interscript/isc/model.rb +19 -0
  55. data/lib/interscript/isc/node_adapter.rb +310 -0
  56. data/lib/interscript/isc/normalizer.rb +127 -0
  57. data/lib/interscript/isc/parser.rb +78 -0
  58. data/lib/interscript/isc/serializer.rb +248 -0
  59. data/lib/interscript/isc/transform.rb +138 -0
  60. data/lib/interscript/isc/yaml_bridge.rb +236 -0
  61. data/lib/interscript/isc.rb +30 -0
  62. data/lib/interscript/ml/byt5_onnx.rb +102 -0
  63. data/lib/interscript/ml/imf.rb +182 -0
  64. data/lib/interscript/ml/model.rb +52 -0
  65. data/lib/interscript/ml/provisioning.rb +189 -0
  66. data/lib/interscript/ml/translator.rb +38 -0
  67. data/lib/interscript/ml/vocab.rb +48 -0
  68. data/lib/interscript/ml.rb +25 -0
  69. data/lib/interscript/node/alias_def.rb +5 -5
  70. data/lib/interscript/node/dependency.rb +7 -7
  71. data/lib/interscript/node/document.rb +10 -10
  72. data/lib/interscript/node/group.rb +11 -9
  73. data/lib/interscript/node/item/alias.rb +14 -10
  74. data/lib/interscript/node/item/any.rb +14 -7
  75. data/lib/interscript/node/item/capture.rb +14 -9
  76. data/lib/interscript/node/item/group.rb +22 -17
  77. data/lib/interscript/node/item/repeat.rb +3 -3
  78. data/lib/interscript/node/item/stage.rb +4 -5
  79. data/lib/interscript/node/item/string.rb +18 -13
  80. data/lib/interscript/node/item.rb +27 -17
  81. data/lib/interscript/node/metadata.rb +6 -5
  82. data/lib/interscript/node/rule/funcall.rb +4 -5
  83. data/lib/interscript/node/rule/run.rb +4 -5
  84. data/lib/interscript/node/rule/sub.rb +55 -55
  85. data/lib/interscript/node/rule.rb +6 -5
  86. data/lib/interscript/node/stage.rb +11 -14
  87. data/lib/interscript/node/tests.rb +6 -6
  88. data/lib/interscript/node.rb +14 -17
  89. data/lib/interscript/stdlib/functions/rababa_adapter.rb +56 -0
  90. data/lib/interscript/stdlib/functions/secryst_adapter.rb +37 -0
  91. data/lib/interscript/stdlib/functions.rb +49 -0
  92. data/lib/interscript/stdlib.rb +25 -95
  93. data/lib/interscript/utils/helpers.rb +8 -5
  94. data/lib/interscript/utils/regexp_converter.rb +143 -144
  95. data/lib/interscript/version.rb +1 -1
  96. data/lib/interscript/visualize/json.rb +12 -13
  97. data/lib/interscript/visualize/nodes.rb +13 -13
  98. data/lib/interscript/visualize.rb +18 -18
  99. data/lib/interscript.rb +59 -45
  100. metadata +88 -31
  101. data/.github/workflows/assets.yml +0 -91
  102. data/.github/workflows/rake.yml +0 -52
  103. data/.github/workflows/release.yml +0 -68
  104. data/.gitignore +0 -79
  105. data/.rspec +0 -3
  106. data/Gemfile +0 -38
  107. data/Rakefile +0 -139
  108. data/bin/console +0 -10
  109. data/bin/interscript +0 -5
  110. data/bin/maps_analyze_staging +0 -168
  111. data/bin/maps_debug_compilers +0 -58
  112. data/bin/maps_debug_ordering +0 -88
  113. data/bin/maps_debug_ruby_compile +0 -24
  114. data/bin/maps_debug_step_by_step +0 -44
  115. data/bin/maps_optimize_order +0 -112
  116. data/bin/maps_v1_analyze_regexps +0 -45
  117. data/bin/maps_v1_to_v2 +0 -426
  118. data/bin/set_version +0 -16
  119. data/bin/setup +0 -8
  120. data/requirements.txt +0 -1
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "parsanol"
4
+
5
+ module Interscript
6
+ module Isc
7
+ autoload :Parser, "interscript/isc/parser"
8
+ autoload :Transform, "interscript/isc/transform"
9
+ autoload :DocumentBuilder, "interscript/isc/document_builder"
10
+ autoload :Grammar, "interscript/isc/grammar"
11
+ autoload :Items, "interscript/isc/items"
12
+ autoload :Codemod, "interscript/isc/codemod"
13
+ autoload :NodeAdapter, "interscript/isc/node_adapter"
14
+ autoload :Model, "interscript/isc/model"
15
+ autoload :YamlBridge, "interscript/isc/yaml_bridge"
16
+ autoload :Serializer, "interscript/isc/serializer"
17
+ autoload :Normalizer, "interscript/isc/normalizer"
18
+ autoload :Generator, "interscript/isc/generator"
19
+
20
+ SCHEMA_VERSION = 1
21
+
22
+ def self.parse(source, filename: nil)
23
+ Parser.parse(source, filename: filename)
24
+ end
25
+
26
+ def self.load_file(path)
27
+ parse(File.read(path, encoding: "UTF-8"), filename: path)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,102 @@
1
+ require 'tmpdir'
2
+
3
+ module Interscript::ML
4
+ # Byte-level seq2seq (ByT5 family) inference over ONNX sessions from an
5
+ # IMF v1 model zip: sha256-verified graphs, greedy KV-cache decode when
6
+ # the zip ships decoder-kv.onnx (default), plain full-recompute
7
+ # fallback otherwise. Tokenization is the canonical ByT5 table
8
+ # (byte b -> id b+3, trailing EOS) — see Interscript::ML::IMF.
9
+ class Byt5Onnx
10
+ def initialize(zip_path)
11
+ Interscript::ML.require_optional!("onnxruntime")
12
+ @manifest = IMF.manifest(zip_path)
13
+ graphs = IMF.verify_and_read(zip_path)
14
+ @tmpdir = Dir.mktmpdir('secryst-imf')
15
+ @encoder = build_session(graphs, 'encoder.onnx')
16
+ @kv = @manifest['decoder'] == 'kv' && graphs.key?('decoder-kv.onnx')
17
+ @decoder = build_session(graphs, @kv ? 'decoder-kv.onnx' : 'decoder.onnx')
18
+ @pasts = @kv ? zero_pasts : {}
19
+ end
20
+
21
+ def translate(text, max_seq_length: 256)
22
+ ids = IMF.encode(text)
23
+ return '' if ids.length == 1 # only the trailing EOS: empty input
24
+
25
+ hidden = @encoder.predict({ input_ids: [ids] })['last_hidden_state']
26
+ tokens = @kv ? greedy_kv(hidden, max_seq_length) : greedy_plain(hidden, max_seq_length)
27
+ IMF.decode(tokens)
28
+ end
29
+
30
+ def id
31
+ @manifest['id']
32
+ end
33
+
34
+ private
35
+
36
+ # The onnxruntime gem loads from paths, not bytes: verified bytes go
37
+ # to a private tmpdir.
38
+ def build_session(graphs, name)
39
+ path = File.join(@tmpdir, name)
40
+ File.binwrite(path, graphs.fetch(name))
41
+ OnnxRuntime::Model.new(path)
42
+ end
43
+
44
+ def zero_pasts
45
+ @decoder.inputs.each_with_object({}) do |meta, pasts|
46
+ next unless meta[:name].start_with?('past_')
47
+ shape = meta[:shape] # [batch, heads, past_seq, d_kv]; dynamic dims are Strings
48
+ heads = shape[1].is_a?(Integer) ? shape[1] : 4
49
+ d_kv = shape[3].is_a?(Integer) ? shape[3] : 8
50
+ # Numo carries the explicit [1, heads, 0, d_kv] shape through;
51
+ # nested Ruby arrays cannot express a zero-length dim.
52
+ pasts[meta[:name]] = Numo::DFloat.zeros(1, heads, 0, d_kv)
53
+ end
54
+ end
55
+
56
+ def argmax(row)
57
+ best = 0
58
+ best_val = -Float::INFINITY
59
+ row.each_with_index do |value, index|
60
+ if value > best_val
61
+ best_val = value
62
+ best = index
63
+ end
64
+ end
65
+ best
66
+ end
67
+
68
+ def greedy_kv(hidden, max_seq_length)
69
+ pasts = @pasts.dup
70
+ current = [IMF::PAD_ID]
71
+ generated = []
72
+ max_seq_length.times do
73
+ results = @decoder.predict(
74
+ { input_ids: [current], encoder_hidden_states: hidden }.merge(pasts)
75
+ )
76
+ token = argmax(results['logits'].first.last)
77
+ break if token == IMF::EOS_ID
78
+
79
+ generated << token
80
+ pasts = pasts.keys.to_h { |name| [name, results[name.sub('past_', 'present_')]] }
81
+ current = [token]
82
+ end
83
+ generated
84
+ end
85
+
86
+ def greedy_plain(hidden, max_seq_length)
87
+ decoder_ids = [IMF::PAD_ID]
88
+ generated = []
89
+ max_seq_length.times do
90
+ logits = @decoder.predict(
91
+ { input_ids: [decoder_ids], encoder_hidden_states: hidden }
92
+ )['logits']
93
+ token = argmax(logits.first.last)
94
+ break if token == IMF::EOS_ID
95
+
96
+ generated << token
97
+ decoder_ids = decoder_ids + [token]
98
+ end
99
+ generated
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,182 @@
1
+ require "yaml"
2
+ require "digest/sha2"
3
+ require "fileutils"
4
+ require "open-uri"
5
+
6
+ module Interscript::ML
7
+ # Interscript Model Format v1 — the byte-level runtime contract shared
8
+ # with the Python (interscript-ml) and TypeScript (npm: secryst)
9
+ # runtimes. Token ids follow the canonical ByT5 table: byte b -> b+3,
10
+ # trailing EOS; pad=0, unk=2. Ids are NOT raw byte values.
11
+ module IMF
12
+ BYTE_OFFSET = 3
13
+ PAD_ID = 0
14
+ EOS_ID = 1
15
+ UNK_ID = 2
16
+
17
+ DEFAULT_INDEX_URL = "https://github.com/interscript/interscript-ml/releases/download/index-v7/models-index.yaml"
18
+
19
+ class FormatError < StandardError; end
20
+ class RegistryError < StandardError; end
21
+
22
+ class << self
23
+ def require_zip!
24
+ Interscript::ML.require_optional!("rubyzip", feature: "zip")
25
+ end
26
+
27
+ def encode(text)
28
+ text.bytes.map { |b| b + BYTE_OFFSET } + [EOS_ID]
29
+ end
30
+
31
+ def decode(token_ids)
32
+ out = +""
33
+ token_ids.each do |token|
34
+ break if token == EOS_ID
35
+ next if token == PAD_ID || token == UNK_ID
36
+ out << ((token - BYTE_OFFSET) % 256).chr
37
+ end
38
+ out.force_encoding(Encoding::UTF_8)
39
+ end
40
+
41
+ def manifest(zip_path)
42
+ require_zip!
43
+ Zip::File.open(zip_path) do |zf|
44
+ raise FormatError, "missing metadata.yaml" unless zf.find_entry("metadata.yaml")
45
+ meta = YAML.safe_load(zf.read("metadata.yaml"), permitted_classes: [], aliases: false)
46
+ raise FormatError, "unsupported format: #{meta["format"].inspect}" if meta["format"] != "imf-v1"
47
+ if meta["tokenizer"] != "bytes"
48
+ raise FormatError, "tokenizer #{meta["tokenizer"].inspect}: this runtime is byte-level only"
49
+ end
50
+ %w[encoder.onnx decoder.onnx].each do |required|
51
+ raise FormatError, "missing #{required}" unless zf.find_entry(required)
52
+ end
53
+ meta
54
+ end
55
+ end
56
+
57
+ # Reads every .onnx member after verifying its sha256 against the
58
+ # manifest — corrupt zips fail loudly, before any session loads.
59
+ def verify_and_read(zip_path)
60
+ meta = manifest(zip_path)
61
+ sha = meta.fetch("sha256", {})
62
+ graphs = {}
63
+ require_zip!
64
+ Zip::File.open(zip_path) do |zf|
65
+ zf.entries.select { |e| e.name.end_with?(".onnx") }.each do |entry|
66
+ recorded = sha[entry.name]
67
+ raise FormatError, "#{entry.name} is not covered by metadata sha256" unless recorded
68
+ bytes = entry.get_input_stream.read
69
+ actual = Digest::SHA256.hexdigest(bytes)
70
+ if actual != recorded
71
+ raise FormatError, "#{entry.name} sha256 mismatch: zip has #{actual}, metadata says #{recorded}"
72
+ end
73
+ graphs[entry.name] = bytes
74
+ end
75
+ end
76
+ graphs
77
+ end
78
+
79
+ def cache_dir
80
+ ENV["INTERSCRIPT_ML_CACHE"] || ENV["SECRYST_CACHE"] || File.join(Dir.home, ".cache", "interscript-ml")
81
+ end
82
+
83
+ # models.yaml resolution: cache hit (re-verified), or download ->
84
+ # verify whole-file sha256 -> atomic install into the cache.
85
+ # Entries with `parts` (GitHub's 2 GiB per-asset cap) are streamed
86
+ # in order, each part sha256-verified as it lands, then the
87
+ # assembled file is checked against the whole-file sha256.
88
+ def resolve(model_id, index_url: nil)
89
+ source = index_url || ENV["INTERSCRIPT_ML_INDEX"] || ENV["SECRYST_INDEX"] || DEFAULT_INDEX_URL
90
+ entries = load_index(source)
91
+ entry = entries[model_id]
92
+ raise RegistryError, "unknown model id #{model_id.inspect} (known: #{entries.keys.sort})" unless entry
93
+
94
+ target = File.join(cache_dir, "models", model_id, entry["filename"])
95
+ if File.file?(target) && Digest::SHA256.file(target).hexdigest == entry["sha256"]
96
+ return target
97
+ end
98
+
99
+ FileUtils.mkdir_p(File.dirname(target))
100
+ tmp = target + ".part.#{Process.pid}"
101
+ if entry["parts"]
102
+ download_parts(entry, tmp)
103
+ else
104
+ channel = entry["url"]
105
+ if channel.start_with?("file://")
106
+ FileUtils.cp(channel.sub(%r{\Afile://}, ""), tmp)
107
+ else
108
+ URI.open(channel) { |remote| IO.copy_stream(remote, tmp) }
109
+ end
110
+ end
111
+ actual = Digest::SHA256.file(tmp).hexdigest
112
+ unless actual == entry["sha256"]
113
+ File.delete(tmp)
114
+ raise RegistryError, "downloaded #{entry["filename"]} sha256 mismatch: got #{actual}, index says #{entry["sha256"]}"
115
+ end
116
+ File.rename(tmp, target)
117
+ target
118
+ end
119
+
120
+ private
121
+
122
+ def download_parts(entry, tmp)
123
+ File.open(tmp, "wb") do |out|
124
+ entry["parts"].each_with_index do |part, index|
125
+ digest = Digest::SHA256.new
126
+ source = part["url"].start_with?("file://") ? part["url"].sub(%r{\Afile://}, "") : part["url"]
127
+ open_stream = lambda do |io|
128
+ while (chunk = io.read(1024 * 1024))
129
+ out.write(chunk)
130
+ digest.update(chunk)
131
+ end
132
+ end
133
+ if part["url"].start_with?("file://")
134
+ File.open(source, "rb", &open_stream)
135
+ else
136
+ URI.open(source, "rb", &open_stream)
137
+ end
138
+ unless digest.hexdigest == part["sha256"]
139
+ raise RegistryError, "part #{index} of #{entry["filename"]} sha256 mismatch: got #{digest.hexdigest}, index says #{part["sha256"]}"
140
+ end
141
+ end
142
+ end
143
+ rescue StandardError
144
+ File.delete(tmp) if File.file?(tmp)
145
+ raise
146
+ end
147
+
148
+ def load_index(source)
149
+ text = if source.start_with?("http://", "https://")
150
+ fetch_index_with_sidecar(source)
151
+ else
152
+ File.read(source)
153
+ end
154
+ raw = YAML.safe_load(text, permitted_classes: [], aliases: false)
155
+ raise RegistryError, "index must have version: 1" if raw["version"] != 1
156
+ raw.fetch("models", {})
157
+ end
158
+ public :load_index
159
+
160
+ # HTTP sources must ship a sibling .sha256 sidecar; verify the
161
+ # body against it before parsing (GitHub Releases index assets do).
162
+ def fetch_index_with_sidecar(source)
163
+ body = URI.open(source, "rb") { |remote| remote.read }
164
+ sidecar =
165
+ begin
166
+ URI.open("#{source}.sha256") { |remote| remote.read }
167
+ rescue OpenURI::HTTPError => e
168
+ raise RegistryError, "index sha256 sidecar missing: #{source}.sha256 (#{e.message})"
169
+ end
170
+ expected = sidecar.to_s.strip.split(/\s+/).first.to_s
171
+ unless expected.match?(/\A[0-9a-fA-F]{64}\z/)
172
+ raise RegistryError, "index sha256 sidecar malformed: #{source}.sha256"
173
+ end
174
+ actual = Digest::SHA256.hexdigest(body)
175
+ unless actual == expected.downcase
176
+ raise RegistryError, "index sha256 mismatch: got #{actual}, sidecar says #{expected.downcase}"
177
+ end
178
+ body.force_encoding(Encoding::UTF_8)
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,52 @@
1
+ module Interscript::ML
2
+ class Model
3
+ attr_accessor :model, :input_vocab, :target_vocab
4
+
5
+ def self.from_file(model_file)
6
+ # A models.yaml model id resolves (download -> verify -> cache)
7
+ # through the same contract as the Python/TS runtimes.
8
+ model_file = IMF.resolve(model_file) unless model_file.to_s.end_with?('.zip') || File.file?(model_file.to_s)
9
+ model_file = Provisioning.locate(model_file)
10
+
11
+ Zip::File.open(model_file) do |zip_file|
12
+ metadata = zip_file.glob('metadata.yaml').first
13
+ metadata = YAML.safe_load(metadata.get_input_stream.read) if metadata
14
+
15
+ # IMF v1: the Interscript Model Format zip.
16
+ return Byt5Onnx.new(model_file) if metadata && metadata['format'] == 'imf-v1'
17
+
18
+ name = metadata && metadata['name']
19
+
20
+ # Modern byte-level seq2seq (ByT5 family): encoder.onnx + decoder.onnx.
21
+ return Byt5Onnx.new(model_file) if name == 'byt5'
22
+
23
+ # Legacy single-file ONNX transformer zips (vocabs.yaml based).
24
+ vocabs = zip_file.glob('vocabs.yaml').first
25
+ raise 'vocabs.yaml is missing in model zip!' unless vocabs
26
+ vocabs = YAML.safe_load(vocabs.get_input_stream.read)
27
+ input_vocab = Vocab.new(vocabs['input'], specials: [])
28
+ target_vocab = Vocab.new(vocabs['target'], specials: [])
29
+
30
+ onnx = zip_file.glob('*.onnx').first
31
+ raise 'onnx model file is missing in model zip!' unless onnx
32
+ Onnx.new(onnx.get_input_stream.read, input_vocab, target_vocab)
33
+ end
34
+ end
35
+
36
+ class Onnx < Model
37
+ def initialize(model_path_or_bytes, input_vocab, target_vocab)
38
+ @model = OnnxRuntime::Model.new(model_path_or_bytes)
39
+ @input_vocab = input_vocab
40
+ @target_vocab = target_vocab
41
+ end
42
+
43
+ def call(input, output, opts)
44
+ @model.predict({ src: input, tgt: output }.merge(opts))['output']
45
+ end
46
+
47
+ def argmax(*args)
48
+ self.call(*args).map { |i| i.flatten.each_with_index.max[1] }
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,189 @@
1
+ require 'open-uri'
2
+ require 'uri'
3
+ require 'fileutils'
4
+ require 'digest/sha2'
5
+ require 'yaml'
6
+
7
+ module Interscript::ML
8
+ # Module Interscript::ML::Provisioning is to provision remote models locally and to
9
+ # dispatch them later on.
10
+ module Provisioning
11
+ extend self
12
+
13
+ @remotes = [] # Here's a place for a global model repository
14
+ @preload_models = []
15
+ attr_accessor :remotes, :preload_models
16
+
17
+ def add_remote(path)
18
+ @remotes << path
19
+ @remotes = @remotes.uniq
20
+ end
21
+
22
+ def prepare_environment
23
+ return if @set_up
24
+
25
+ # We provision the environment in the following way:
26
+ # First, we try the SECRYST_DATA environment variable. If that's available,
27
+ # we use it to store the Secryst data we need. Otherwise, we try the following
28
+ # paths:
29
+
30
+ possible_paths = [
31
+ "/var/lib/secryst",
32
+ "/usr/local/share/secryst",
33
+ "/usr/share/secryst",
34
+ File.join(Dir.home, ".local/share/secryst")
35
+ ]
36
+
37
+ # We find the first writable path to become the primary one. The remaining
38
+ # ones will be used read-only if they exist
39
+
40
+ @write_path = nil
41
+ @read_paths = []
42
+
43
+ ([ENV["INTERSCRIPT_ML_DATA"] || ENV["SECRYST_DATA"]] + possible_paths).compact.each do |path|
44
+ FileUtils.mkdir_p(path)
45
+ @write_path = path unless @write_path
46
+ rescue
47
+ ensure
48
+ @read_paths << path if File.readable?(path)
49
+ end
50
+
51
+ raise StandardError, "Can't find a writable path for Secryst. Consider setting a SECRYST_DATA environment variable" unless @write_path
52
+
53
+ # Now, let's locate the first Secrystfile to be found
54
+ path = Dir.pwd
55
+ secrystfilepath = loop do
56
+ break unless path =~ %r{[/\\]}
57
+ if File.readable?(path + "/Secrystfile")
58
+ break path + "/Secrystfile"
59
+ end
60
+ path = path.sub(%r{[/\\][^/\\]*?\z}, '')
61
+ end
62
+
63
+ # It's found, so let's parse it.
64
+ if secrystfilepath
65
+ secrystfile = Secrystfile.new(secrystfilepath)
66
+
67
+ @remotes = secrystfile.remotes + @remotes
68
+ @remotes = @remotes.uniq
69
+
70
+ @preload_models = secrystfile.models
71
+ end
72
+
73
+ # Load the remotes if they are older than 1 minute
74
+ FileUtils.mkdir_p(@write_path + "/remotes/")
75
+ @loaded_remotes = @remotes.map do |uri|
76
+ cache_path = "#{@write_path}/remotes/#{Digest::SHA256.hexdigest(uri)}.yaml"
77
+ if !File.exist?(cache_path)
78
+ data = URI.open(uri).read
79
+ File.write(cache_path, data)
80
+ elsif File.mtime(cache_path) + 60 < Time.now
81
+ begin
82
+ # Just *try* to download it.
83
+ data = URI.open(uri).read
84
+ File.write(cache_path, data)
85
+ rescue
86
+ end
87
+ end
88
+ Remotefile.new(cache_path, uri: uri)
89
+ end
90
+
91
+ # Ok we are done now. We still need to resolve the required paths, but for
92
+ # that let's reuse our existing facilities.
93
+ @set_up = true
94
+
95
+ @preload_models.each { |i| locate(i) }
96
+ end
97
+
98
+ def locate(name)
99
+ # Shortcut this if user gave a filename.
100
+ return name if name =~ %r{[.\\/]}
101
+
102
+ prepare_environment
103
+
104
+ @loaded_remotes.each do |i|
105
+ model = i.resolve(name)
106
+ if model
107
+ model_path = @read_paths.map do |j|
108
+ path = j + "/models/" + model.name
109
+ next path if File.readable?(path)
110
+ nil
111
+ end.compact.first
112
+
113
+ if model_path
114
+ version = File.read(model_path + "/version").to_f
115
+ if version >= model.version
116
+ return model_path + "/model.zip"
117
+ else
118
+ return download(model, remote: i)
119
+ end
120
+ else
121
+ return download(model, remote: i)
122
+ end
123
+ end
124
+ end
125
+
126
+ raise StandardError, "Model #{name} not found"
127
+ end
128
+
129
+ def download(model, remote:)
130
+ uri = model.uri
131
+ if uri.start_with?("./") || uri.start_with?("../")
132
+ remote_uri = remote.uri
133
+ if remote_uri =~ %r{\A/|\A\w:[\\/]}
134
+ remote_uri = "file://"+remote_uri
135
+ end
136
+
137
+ uri = URI(remote_uri).merge(uri).to_s
138
+ uri = uri.sub(%r{\Afile://}, '')
139
+ end
140
+
141
+ warn "* Downloading a Secryst model #{model.name}:#{model.version} from #{uri}..."
142
+
143
+ data = URI.open(uri).read
144
+ path = @write_path + "/models/" + model.name
145
+ FileUtils.mkdir_p path
146
+ File.write(path + "/version", model.version)
147
+ File.write(path + "/model.zip", data)
148
+ return path + "/model.zip"
149
+ end
150
+
151
+ class Remotefile
152
+ attr_accessor :uri
153
+
154
+ def initialize(path, uri:)
155
+ @yaml = YAML.load_file(path)
156
+ @uri = uri
157
+ end
158
+
159
+ def resolve(name)
160
+ @yaml["models"].find do |n, desc|
161
+ if name == n
162
+ return Model.new(name: n, uri: desc["path"], version: desc["version"])
163
+ end
164
+ end
165
+ end
166
+
167
+ class Model < Struct.new(:name, :uri, :version, keyword_init: true)
168
+ end
169
+ end
170
+
171
+ class Secrystfile
172
+ def initialize(path)
173
+ @path = path
174
+ @models, @remotes = [], []
175
+ self.instance_eval(File.read(path), path)
176
+ end
177
+
178
+ def model(name)
179
+ @models << name
180
+ end
181
+
182
+ def source(src)
183
+ @remotes << src
184
+ end
185
+
186
+ attr_accessor :models, :remotes
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,38 @@
1
+ module Interscript::ML
2
+ # Dispatching translator: byte-level ByT5 models (name: byt5 in
3
+ # metadata.yaml) translate themselves; legacy char-vocab ONNX zips use
4
+ # the original greedy loop.
5
+ class Translator
6
+ attr_accessor :model
7
+
8
+ def initialize(model_file:)
9
+ @device = 'cpu'
10
+ @model = Model.from_file(model_file)
11
+ end
12
+
13
+ def translate(phrase, max_seq_length: 100)
14
+ return @model.translate(phrase, max_seq_length: max_seq_length) if @model.is_a?(Byt5Onnx)
15
+
16
+ Interscript::ML.require_optional!("numo-narray", feature: "numo/narray")
17
+ input = ['<sos>'] + phrase.chars + ['<eos>']
18
+ input = Numo::NArray[input.map { |i| @model.input_vocab.stoi[i] }].transpose
19
+ output = Numo::NArray[[@model.target_vocab.stoi['<sos>']]]
20
+ src_key_padding_mask = input.transpose.eq(1)
21
+
22
+ max_seq_length.times do |i|
23
+ tgt_key_padding_mask = output.transpose.eq(1)
24
+ tgt_mask = Numo::DFloat.ones(i + 1, i + 1).triu.transpose.eq(0)
25
+ prediction = @model.argmax(input, output.dup,
26
+ tgt_mask: tgt_mask,
27
+ src_key_padding_mask: src_key_padding_mask,
28
+ tgt_key_padding_mask: tgt_key_padding_mask,
29
+ memory_key_padding_mask: src_key_padding_mask)
30
+ break if @model.target_vocab.itos[prediction[i]] == '<eos>'
31
+
32
+ output = Numo::NArray.concatenate([output, Numo::NArray[[prediction[i]]]])
33
+ end
34
+
35
+ output[1..-1].to_a.flatten.map { |i| @model.target_vocab.itos[i] }.join('')
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,48 @@
1
+ module Interscript::ML
2
+ class Vocab
3
+ UNK = "<unk>"
4
+ attr_reader :stoi, :itos
5
+
6
+ def initialize(
7
+ list, specials: ["<unk>", "<pad>", "<sos>", "<eos>"], specials_first: true
8
+ )
9
+ @unk_index = nil
10
+ @itos = []
11
+ if specials_first && (list & specials).length == 0
12
+ @itos = specials
13
+ end
14
+
15
+ @itos += list
16
+
17
+ if !specials_first && (list & specials).length == 0
18
+ @itos.concat(specials)
19
+ end
20
+
21
+ # Automatic substitution of unknown symbols
22
+ if @itos.include?("<unk>")
23
+ unk_index = @itos.index("<unk>")
24
+ @stoi = Hash.new(unk_index)
25
+ elsif @itos.include?("[UNK]")
26
+ unk_index = @itos.index("[UNK]")
27
+ @stoi = Hash.new(unk_index)
28
+ else
29
+ @stoi = {}
30
+ end
31
+
32
+
33
+ # stoi is simply a reverse dict for itos
34
+ @itos.each_with_index do |tok, i|
35
+ @stoi[tok] = i
36
+ end
37
+ end
38
+
39
+ def [](token)
40
+ @stoi.fetch(token, @stoi.fetch(UNK))
41
+ end
42
+
43
+ def length
44
+ @itos.length
45
+ end
46
+ alias_method :size, :length
47
+ end
48
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The neural layer: IMF v1 artifact runtime + index provisioning for
4
+ # byte-level ByT5 models, ported from the secryst crystal (1.1.1) into
5
+ # interscript proper. Runtime dependencies rubyzip and onnxruntime are
6
+ # OPTIONAL: they load lazily with a helpful error when absent.
7
+ #
8
+ # Interscript::ML::Translator.new(model_file: "khm-latn-1.0")
9
+ # .translate("ភាសា")
10
+ module Interscript::ML
11
+ autoload :IMF, "interscript/ml/imf"
12
+ autoload :Provisioning, "interscript/ml/provisioning"
13
+ autoload :Translator, "interscript/ml/translator"
14
+ autoload :Model, "interscript/ml/model"
15
+ autoload :Vocab, "interscript/ml/vocab"
16
+ autoload :Byt5Onnx, "interscript/ml/byt5_onnx"
17
+
18
+ # Expected to be loaded before IMF/Byt5Onnx entry points.
19
+ def self.require_optional!(gem_name, feature: gem_name)
20
+ require feature
21
+ rescue LoadError => e
22
+ raise Interscript::ExternalUtilError,
23
+ "the neural layer needs the #{gem_name} gem: gem install #{gem_name} (or use a bundle including it)"
24
+ end
25
+ end
@@ -9,13 +9,13 @@ class Interscript::Node::AliasDef < Interscript::Node
9
9
 
10
10
  def ==(other)
11
11
  super &&
12
- self.name == other.name &&
13
- self.data == other.data
12
+ name == other.name &&
13
+ data == other.data
14
14
  end
15
15
 
16
16
  def to_hash
17
- { :class => self.class.to_s,
18
- :name => @name,
19
- :data => @data.to_hash }
17
+ {class: self.class.to_s,
18
+ name: @name,
19
+ data: @data.to_hash}
20
20
  end
21
21
  end