| 1 | # Hugging Face-style discovery page for open-weights model repositories. |
| 2 | class ModelsController < ApplicationController |
| 3 | def index |
| 4 | @query = params[:q].to_s.strip |
| 5 | @pipeline_tag = params[:pipeline_tag].presence |
| 6 | @library = params[:library].presence |
| 7 | @sort = params.fetch(:sort, "trending") |
| 8 | |
| 9 | repos = Repository.models |
| 10 | .visible_to(current_user) |
| 11 | .includes(:user) |
| 12 | .to_a |
| 13 | |
| 14 | # Decorate each repo with its parsed model card once, then filter/sort in |
| 15 | # Ruby — the card metadata lives in git, not the database. |
| 16 | cards = repos.map { |repo| [ repo, repo.model_card ] } |
| 17 | |
| 18 | if @query.present? |
| 19 | q = @query.downcase |
| 20 | cards.select! do |repo, card| |
| 21 | repo.full_name.downcase.include?(q) || |
| 22 | repo.description.to_s.downcase.include?(q) || |
| 23 | card.tags.any? { |t| t.downcase.include?(q) } |
| 24 | end |
| 25 | end |
| 26 | |
| 27 | @facets = build_facets(cards) |
| 28 | |
| 29 | cards.select! { |_repo, card| card.pipeline_tag == @pipeline_tag } if @pipeline_tag |
| 30 | cards.select! { |_repo, card| card.library_name == @library } if @library |
| 31 | |
| 32 | @models = sort_cards(cards) |
| 33 | end |
| 34 | |
| 35 | private |
| 36 | |
| 37 | def build_facets(cards) |
| 38 | { |
| 39 | pipeline_tags: tally(cards) { |_repo, card| card.pipeline_tag }, |
| 40 | libraries: tally(cards) { |_repo, card| card.library_name } |
| 41 | } |
| 42 | end |
| 43 | |
| 44 | def tally(cards) |
| 45 | cards.filter_map { |pair| yield(*pair) } |
| 46 | .tally |
| 47 | .sort_by { |label, count| [ -count, label ] } |
| 48 | end |
| 49 | |
| 50 | def sort_cards(cards) |
| 51 | case @sort |
| 52 | when "downloads" |
| 53 | cards.sort_by { |repo, _| -repo.downloads_count } |
| 54 | when "likes" |
| 55 | cards.sort_by { |repo, _| -repo.stars_count } |
| 56 | when "recent" |
| 57 | cards.sort_by { |repo, _| repo.updated_at }.reverse |
| 58 | else # "trending" — a blend of likes, downloads and recency |
| 59 | cards.sort_by { |repo, _| -trending_score(repo) } |
| 60 | end |
| 61 | end |
| 62 | |
| 63 | def trending_score(repo) |
| 64 | age_days = [ (Time.current - repo.updated_at) / 1.day, 0.5 ].max |
| 65 | ((repo.stars_count * 3) + Math.log10(repo.downloads_count + 1) * 10) / age_days |
| 66 | end |
| 67 | end |