scripts/generation-hf-enrich.rb
Rama main · réplica de lectura del repositorio original.
require 'json'
require 'net/http'
require 'time'
require 'uri'
MAX_RESPONSE = 4 * 1024 * 1024
KINDS = {
'image' => { 'pipeline' => 'text-to-image', 'search' => 'flux1-schnell' },
'video' => { 'pipeline' => 'text-to-video', 'search' => 'wan2.1-t2v' }
}.freeze
VARIANTS = {
'image' => [{
'base' => 'flux1-schnell:fp8',
'id' => 'flux1-schnell:fp16',
'workflow' => 'flux-split',
'files' => [
{
'role' => 'diffusion', 'folder' => 'diffusion_models',
'repo' => 'Comfy-Org/flux1-schnell', 'file' => 'flux1-schnell.safetensors'
},
{
'role' => 'text-encoder', 'folder' => 'text_encoders',
'repo' => 'comfyanonymous/flux_text_encoders',
'file' => 't5xxl_fp8_e4m3fn_scaled.safetensors'
},
{
'role' => 'clip', 'folder' => 'text_encoders',
'repo' => 'comfyanonymous/flux_text_encoders', 'file' => 'clip_l.safetensors'
},
{
'role' => 'vae', 'folder' => 'vae',
'repo' => 'Comfy-Org/Lumina_Image_2.0_Repackaged',
'file' => 'split_files/vae/ae.safetensors'
}
]
}],
'video' => [{
'base' => 'wan2.1-t2v:1.3b',
'id' => 'wan2.1-t2v:1.3b-bf16',
'files' => {
'diffusion' => 'split_files/diffusion_models/wan2.1_t2v_1.3B_bf16.safetensors'
}
}]
}.freeze
def repository?(value)
value.is_a?(String) &&
value.match?(/\A[A-Za-z0-9][A-Za-z0-9._-]*\/[A-Za-z0-9][A-Za-z0-9._-]*\z/)
end
def fetch_json(path, query = nil)
raise 'unsafe Hugging Face path' unless path.start_with?('/api/models/') || path == '/api/models'
uri = URI::HTTPS.build(host: 'huggingface.co', path: path, query: query && URI.encode_www_form(query))
request = Net::HTTP::Get.new(uri)
request['User-Agent'] = 'RigSpark-Generation-Enrichment/1.0'
token = ENV['HF_TOKEN']
if token && !token.empty?
raise 'invalid HF_TOKEN' if token.bytesize > 4096 || token.match?(/[[:cntrl:]]/)
request['Authorization'] = "Bearer #{token}"
end
body = ''.b
status = nil
Net::HTTP.start(uri.host, 443, nil, use_ssl: true, open_timeout: 10, read_timeout: 60) do |http|
http.request(request) do |response|
status = response.code.to_i
response.read_body do |chunk|
raise 'Hugging Face response exceeds 4 MiB' if body.bytesize + chunk.bytesize > MAX_RESPONSE
body << chunk
end
end
end
raise "Hugging Face HTTP #{status}" unless status == 200
JSON.parse(body)
end
def pinned_metadata(repo, cache)
cache[repo] ||= begin
current = fetch_json("/api/models/#{repo}")
revision = current.fetch('sha')
raise 'invalid immutable revision' unless revision.match?(/\A[0-9a-f]{40}\z/)
metadata = fetch_json("/api/models/#{repo}/revision/#{revision}", { 'blobs' => 'true' })
raise 'revision changed during collection' unless metadata['sha'] == revision
[revision, metadata]
end
end
def lfs_digest(lfs)
digest = lfs['sha256'] || lfs['oid']&.delete_prefix('sha256:')
raise 'invalid weight digest' unless digest&.match?(/\A[0-9a-f]{64}\z/)
digest
end
def enriched_model(model, cache)
copy = Marshal.load(Marshal.dump(model))
source_repo = URI(copy.fetch('source')).path.delete_prefix('/')
raise 'invalid source repository coordinates' unless repository?(source_repo)
_, source_metadata = pinned_metadata(source_repo, cache)
upstream_license = source_metadata.dig('cardData', 'license')
raise 'source license does not match the allowlisted catalog license' unless upstream_license == copy.fetch('license')
copy.fetch('files').each do |file|
repo = file.fetch('repo')
raise 'invalid repository coordinates' unless repository?(repo)
revision, metadata = pinned_metadata(repo, cache)
sibling = metadata.fetch('siblings').find { |entry| entry['rfilename'] == file.fetch('file') }
raise "weight file missing: #{repo}/#{file['file']}" unless sibling
lfs = sibling.fetch('lfs')
digest = lfs_digest(lfs)
bytes = lfs.fetch('size')
raise 'invalid weight size' unless bytes.is_a?(Integer) && bytes.positive?
file['revision'] = revision
file['sha256'] = digest
file['bytes'] = bytes
end
copy
end
def curated_variants(kind, models)
ids = models.map { |model| model.fetch('id') }
VARIANTS.fetch(kind).each_with_object([]) do |variant, variants|
next if ids.include?(variant.fetch('id'))
base = models.find { |model| model['id'] == variant.fetch('base') }
raise "missing curated variant base: #{variant['base']}" unless base
copy = Marshal.load(Marshal.dump(base))
copy['id'] = variant.fetch('id')
copy['default'] = false
if variant['workflow']
copy['workflow'] = variant.fetch('workflow')
copy['files'] = variant.fetch('files').map do |file|
file.merge('revision' => '0' * 40, 'sha256' => '0' * 64, 'bytes' => 1)
end
else
variant.fetch('files').each do |role, file|
target = copy.fetch('files').find { |entry| entry['role'] == role }
raise "missing curated variant role: #{role}" unless target
target['file'] = file
end
end
variants << copy
end
end
if ARGV == ['--self-test']
raise 'accepted unsafe repository' if repository?('../bad')
raise 'rejected valid repository' unless repository?('Comfy-Org/flux1-schnell')
raise 'missing kind configuration' unless KINDS.keys == %w[image video]
raise 'missing curated variants' unless VARIANTS.keys == %w[image video]
digest = 'a' * 64
raise 'current LFS digest schema rejected' unless lfs_digest({ 'sha256' => digest }) == digest
raise 'legacy LFS digest schema rejected' unless lfs_digest({ 'oid' => "sha256:#{digest}" }) == digest
image = { 'id' => 'flux1-schnell:fp8', 'default' => true,
'files' => [{ 'role' => 'checkpoint', 'file' => 'old.safetensors' }] }
variant = curated_variants('image', [image]).first
raise 'curated image variant missing' unless variant['id'] == 'flux1-schnell:fp16'
raise 'curated variant changed default' unless variant['default'] == false
raise 'curated image workflow missing' unless variant['workflow'] == 'flux-split'
raise 'curated variant files missing' unless variant['files'].map { |file| file['role'] } ==
%w[diffusion text-encoder clip vae]
puts 'PASS: generation Hugging Face collector bounds'
exit
end
abort 'Usage: ruby scripts/generation-hf-enrich.rb KIND CATALOG CANDIDATES REPORT' unless ARGV.length == 4
kind, catalog_path, candidates_path, report_path = ARGV
config = KINDS[kind] or abort 'KIND must be image or video'
catalog = JSON.parse(File.read(catalog_path))
models = catalog.fetch('models').select { |model| model['kind'] == kind }
abort "generation catalog has no #{kind} models" if models.empty?
cache = {}
candidates = (models + curated_variants(kind, models)).map { |model| enriched_model(model, cache) }
known_sources = models.map { |model| URI(model.fetch('source')).path.delete_prefix('/') }
discovered = fetch_json('/api/models', {
'search' => config['search'],
'pipeline_tag' => config['pipeline'],
'limit' => '100',
'full' => 'true'
})
raise 'invalid discovery response' unless discovered.is_a?(Array) && discovered.length <= 100
unknown = discovered.each_with_object([]) do |entry, entries|
repo = entry['id']
next if !repository?(repo) || known_sources.include?(repo)
entries << {
'repository' => repo,
'pipelineTag' => entry['pipeline_tag'],
'license' => entry.dig('cardData', 'license'),
'reason' => 'not auto-added: no reviewed built-in workflow mapping'
}
end
File.write(candidates_path, JSON.pretty_generate(candidates) + "\n")
File.write(report_path, JSON.pretty_generate({
'schemaVersion' => 1,
'kind' => kind,
'generatedAt' => Time.now.utc.iso8601,
'requiresReview' => true,
'knownCandidates' => candidates.map { |model| model['id'] },
'unknownCandidates' => unknown
}) + "\n")
warn "generation-hf-enrich: kind=#{kind} known=#{candidates.length} unknown=#{unknown.length}"