世界のAIモデル数
設定難易度

AIモデルって、今どれくらいあるの?

世界で公開されているAIモデルの総数と、新しく公開されたモデルをホーム画面からチェックできるウィジェットです。

現在のモデル総数だけでなく、今年・今月・今週に公開されたAIモデル数も表示。

中・大サイズでは、最近公開されたAIの名前や開発元も確認できます。

「AIってこんなペースで増えてるんだ」と眺めるだけでも、なかなか面白いウィジェットです

ウィジェットの表示内容

Epoch AIのCSVを読み、モデル名がある行を総数として集計します。

サイズごとの表示

  • 小:総モデル数、今年・今月・直近7日間の公開数と、最近公開されたAIを1件表示します。
  • 中:同じ集計値に、最近公開されたAI3件の名前・開発元・公開からの日数を表示します。
  • 大:最近公開されたAI4件と、今年公開されたAIの分野別集計を加えます。

設定手順

上から順に進めてください。コードの保存と設定のコピーに必要な操作欄は、それぞれの手順の中にあります。

  1. Scriptableを用意する

    App StoreでScriptableを開き、iPhoneにインストールします。すでに入っている場合は次へ進んでください。

  2. コードをコピーして保存する

    下のコード欄の「コピー」を押します。Scriptableへ移動し、「+」から新しいスクリプトを作ってコード全文を貼り付け、世界のAIモデル数は?などの名前で保存してください。

    保存したらこのページに戻り、次の手順へ進みます。

    // Variables used by Scriptable.
    // These must be at the very top of the file. Do not edit.
    // icon-color: purple; icon-glyph: brain;
    // Version: 1.0.0
    
    // =====================================================
    // AI MODELS - Scriptable Widget
    //
    // Data: Epoch AI「AI Models」
    //
    // 小:総モデル数
    //     今年 / 今月 / 今週
    //     最近公開されたAI 1件
    //
    // 中:総モデル数
    //     今年 / 今月 / 今週
    //     最近公開されたAI 3件
    //
    // 大:総モデル数
    //     今年 / 今月 / 今週
    //     最近公開されたAI 4件
    //     今年公開されたAI 分野別
    //
    // ・APIキー不要
    // ・自前サーバー不要
    // ・初回ホーム画面から取得可能
    // ・軽量CSV解析
    // ・キャッシュ対応
    // =====================================================
    
    
    // =====================================================
    // 設定
    // =====================================================
    
    const CONFIG = {
    
      CSV_URL:
        "https://epoch.ai/data/all_ai_models.csv",
    
      DATA_URL:
        "https://epoch.ai/data/ai-models",
    
      CACHE_FILE:
        "geboku_epoch_ai_stats_v2.json",
    
      CACHE_HOURS: 6,
    
      BG:
        new Color("#111111"),
    
      TEXT:
        Color.white(),
    
      SUBTEXT:
        new Color("#9A9AA0"),
    
      ACCENT:
        new Color("#A78BFA")
    };
    
    
    // =====================================================
    // キャッシュ
    // =====================================================
    
    const fm =
      FileManager.local();
    
    const CACHE_PATH =
      fm.joinPath(
        fm.documentsDirectory(),
        CONFIG.CACHE_FILE
      );
    
    
    function loadCache() {
    
      try {
    
        if (!fm.fileExists(CACHE_PATH)) {
          return null;
        }
    
        const data =
          JSON.parse(
            fm.readString(CACHE_PATH)
          );
    
        if (!data || !data.stats) {
          return null;
        }
    
        return data;
    
      } catch (e) {
    
        return null;
      }
    }
    
    
    function saveCache(stats) {
    
      try {
    
        fm.writeString(
          CACHE_PATH,
          JSON.stringify({
            timestamp: Date.now(),
            stats
          })
        );
    
      } catch (e) {
    
        console.log(
          "Cache save failed: " + e
        );
      }
    }
    
    
    function isCacheFresh(cache) {
    
      if (!cache) {
        return false;
      }
    
      return (
        Date.now() - cache.timestamp
        <
        CONFIG.CACHE_HOURS *
        60 *
        60 *
        1000
      );
    }
    
    
    // =====================================================
    // CSV 1行解析
    // =====================================================
    
    function parseCSVLine(line) {
    
      const result = [];
    
      let value = "";
      let quoted = false;
    
    
      for (
        let i = 0;
        i < line.length;
        i++
      ) {
    
        const char =
          line[i];
    
    
        if (char === '"') {
    
          if (
            quoted &&
            line[i + 1] === '"'
          ) {
    
            value += '"';
            i++;
    
          } else {
    
            quoted = !quoted;
          }
    
    
        } else if (
          char === "," &&
          !quoted
        ) {
    
          result.push(value);
    
          value = "";
    
    
        } else {
    
          value += char;
        }
      }
    
    
      result.push(value);
    
      return result;
    }
    
    
    // =====================================================
    // CSV論理行分割
    // =====================================================
    
    function splitCSVRows(text) {
    
      const rows = [];
    
      let start = 0;
      let quoted = false;
    
    
      for (
        let i = 0;
        i < text.length;
        i++
      ) {
    
        const char =
          text[i];
    
    
        if (char === '"') {
    
          if (
            quoted &&
            text[i + 1] === '"'
          ) {
    
            i++;
    
          } else {
    
            quoted = !quoted;
          }
    
          continue;
        }
    
    
        if (
          !quoted &&
          (
            char === "\n" ||
            char === "\r"
          )
        ) {
    
          const row =
            text
              .slice(start, i)
              .trim();
    
    
          if (row) {
            rows.push(row);
          }
    
    
          if (
            char === "\r" &&
            text[i + 1] === "\n"
          ) {
    
            i++;
          }
    
    
          start =
            i + 1;
        }
      }
    
    
      if (
        start < text.length
      ) {
    
        const row =
          text
            .slice(start)
            .trim();
    
    
        if (row) {
          rows.push(row);
        }
      }
    
    
      return rows;
    }
    
    
    // =====================================================
    // 日付
    // =====================================================
    
    function parseDate(value) {
    
      if (!value) {
        return null;
      }
    
    
      const match =
        value.match(
          /^(\d{4})-(\d{1,2})-(\d{1,2})/
        );
    
    
      if (match) {
    
        return new Date(
          Number(match[1]),
          Number(match[2]) - 1,
          Number(match[3])
        );
      }
    
    
      const date =
        new Date(value);
    
    
      if (
        isNaN(date.getTime())
      ) {
    
        return null;
      }
    
    
      return date;
    }
    
    
    // =====================================================
    // 最新モデル保持
    // =====================================================
    
    function insertLatest(
      latest,
      item
    ) {
    
      let inserted = false;
    
    
      for (
        let i = 0;
        i < latest.length;
        i++
      ) {
    
        if (
          item.time >
          latest[i].time
        ) {
    
          latest.splice(
            i,
            0,
            item
          );
    
          inserted = true;
    
          break;
        }
      }
    
    
      if (!inserted) {
    
        latest.push(item);
      }
    
    
      if (
        latest.length > 6
      ) {
    
        latest.pop();
      }
    }
    
    
    // =====================================================
    // Domain分割
    // =====================================================
    
    function parseDomains(value) {
    
      if (!value) {
        return [];
      }
    
    
      return value
        .replace(/^\[|\]$/g, "")
        .replace(/["']/g, "")
        .split(/[,;|]/)
        .map(v => v.trim())
        .filter(Boolean);
    }
    
    
    // =====================================================
    // CSV集計
    // =====================================================
    
    function analyzeCSV(csv) {
    
      const rows =
        splitCSVRows(csv);
    
    
      if (
        rows.length < 2
      ) {
    
        throw new Error(
          "CSVデータが空です"
        );
      }
    
    
      const headers =
        parseCSVLine(rows[0])
          .map(v =>
            v
              .replace(/^\uFEFF/, "")
              .trim()
          );
    
    
      const modelIndex =
        headers.indexOf("Model");
    
    
      let dateIndex =
        headers.indexOf(
          "Publication date"
        );
    
    
      if (
        dateIndex < 0
      ) {
    
        dateIndex =
          headers.indexOf(
            "Publication Date"
          );
      }
    
    
      let orgIndex =
        headers.indexOf(
          "Organization"
        );
    
    
      if (
        orgIndex < 0
      ) {
    
        orgIndex =
          headers.indexOf(
            "Organization(s)"
          );
      }
    
    
      const domainIndex =
        headers.indexOf("Domain");
    
    
      if (
        modelIndex < 0 ||
        dateIndex < 0
      ) {
    
        throw new Error(
          "必要な列が見つかりません"
        );
      }
    
    
      const now =
        new Date();
    
    
      const year =
        now.getFullYear();
    
    
      const month =
        now.getMonth();
    
    
      const today =
        new Date(
          year,
          month,
          now.getDate()
        );
    
    
      const weekAgo =
        new Date(today);
    
    
      weekAgo.setDate(
        weekAgo.getDate() - 7
      );
    
    
      let total = 0;
      let thisYear = 0;
      let thisMonth = 0;
      let thisWeek = 0;
    
    
      const latest = [];
    
      const domains = {};
    
    
      // ===================================================
      // 全行走査
      // ===================================================
    
      for (
        let i = 1;
        i < rows.length;
        i++
      ) {
    
        const values =
          parseCSVLine(
            rows[i]
          );
    
    
        const name =
          values[modelIndex];
    
    
        if (!name) {
          continue;
        }
    
    
        total++;
    
    
        const date =
          parseDate(
            values[dateIndex]
          );
    
    
        if (!date) {
          continue;
        }
    
    
        if (
          date > now
        ) {
    
          continue;
        }
    
    
        // =================================================
        // 今年
        // =================================================
    
        if (
          date.getFullYear() ===
          year
        ) {
    
          thisYear++;
    
    
          // ===============================================
          // Domain集計
          // ===============================================
    
          if (
            domainIndex >= 0
          ) {
    
            const modelDomains =
              parseDomains(
                values[domainIndex]
              );
    
    
            for (
              const domain of modelDomains
            ) {
    
              domains[domain] =
                (
                  domains[domain] ||
                  0
                ) + 1;
            }
          }
    
    
          // ===============================================
          // 今月
          // ===============================================
    
          if (
            date.getMonth() ===
            month
          ) {
    
            thisMonth++;
          }
        }
    
    
        // =================================================
        // 今週
        // =================================================
    
        if (
          date >= weekAgo
        ) {
    
          thisWeek++;
        }
    
    
        // =================================================
        // 最新モデル
        // =================================================
    
        insertLatest(
          latest,
          {
    
            name:
              name.trim(),
    
            organization:
              orgIndex >= 0
                ? (
                    values[orgIndex] ||
                    ""
                  ).trim()
                : "",
    
            date:
              date.toISOString(),
    
            time:
              date.getTime()
          }
        );
      }
    
    
      // ===================================================
      // Domainランキング
      // ===================================================
    
      const domainRanking =
        Object.entries(domains)
    
          .sort(
            (a, b) =>
              b[1] - a[1]
          )
    
          .map(
            ([name, count]) => ({
              name,
              count
            })
          );
    
    
      return {
    
        total,
    
        year:
          thisYear,
    
        month:
          thisMonth,
    
        week:
          thisWeek,
    
        latest:
          latest.map(item => ({
    
            name:
              item.name,
    
            organization:
              item.organization,
    
            date:
              item.date
          })),
    
        domains:
          domainRanking
      };
    }
    
    
    // =====================================================
    // ネットワーク
    // =====================================================
    
    async function downloadStats() {
    
      const req =
        new Request(
          CONFIG.CSV_URL
        );
    
    
      req.timeoutInterval = 20;
    
    
      const csv =
        await req.loadString();
    
    
      if (
        !csv ||
        csv.length < 100
      ) {
    
        throw new Error(
          "データを取得できませんでした"
        );
      }
    
    
      return analyzeCSV(csv);
    }
    
    
    // =====================================================
    // データ取得
    // =====================================================
    
    async function getStats() {
    
      const cache =
        loadCache();
    
    
      if (
        isCacheFresh(cache)
      ) {
    
        return cache.stats;
      }
    
    
      try {
    
        const stats =
          await downloadStats();
    
    
        saveCache(stats);
    
    
        return stats;
    
    
      } catch (error) {
    
        if (cache) {
    
          return cache.stats;
        }
    
    
        throw error;
      }
    }
    
    
    // =====================================================
    // ヘッダー
    // =====================================================
    
    function addHeader(
      parent,
      centered = false
    ) {
    
      const row =
        parent.addStack();
    
    
      if (centered) {
    
        row.addSpacer();
      }
    
    
      row.centerAlignContent();
    
    
      const icon =
        row.addText("🤖");
    
    
      icon.font =
        Font.systemFont(13);
    
    
      row.addSpacer(5);
    
    
      const title =
        row.addText(
          "AI MODELS"
        );
    
    
      title.font =
        Font.semiboldSystemFont(11);
    
    
      title.textColor =
        CONFIG.SUBTEXT;
    
    
      if (centered) {
    
        row.addSpacer();
      }
    }
    
    
    // =====================================================
    // メイン数字
    // =====================================================
    
    function addMainNumber(
      parent,
      number,
      size,
      centered = false,
      showUnit = true
    ) {
    
      const row =
        parent.addStack();
    
    
      if (centered) {
    
        row.addSpacer();
      }
    
    
      row.bottomAlignContent();
    
    
      const value =
        row.addText(
          number.toLocaleString(
            "ja-JP"
          )
        );
    
    
      value.font =
        Font.boldRoundedSystemFont(
          size
        );
    
    
      value.textColor =
        CONFIG.TEXT;
    
    
      value.minimumScaleFactor =
        0.7;
    
    
      if (showUnit) {
    
        row.addSpacer(5);
    
    
        const unit =
          row.addText(
            "モデル"
          );
    
    
        unit.font =
          Font.semiboldSystemFont(
            Math.round(
              size * 0.27
            )
          );
    
    
        unit.textColor =
          CONFIG.SUBTEXT;
      }
    
    
      if (centered) {
    
        row.addSpacer();
      }
    }
    
    
    // =====================================================
    // 縦型統計
    // 中・大
    // =====================================================
    
    function addVerticalStats(
      parent,
      stats,
      large = false
    ) {
    
      const column =
        parent.addStack();
    
    
      column.layoutVertically();
    
    
      const items = [
    
        ["今年", stats.year],
        ["今月", stats.month],
        ["今週", stats.week]
    
      ];
    
    
      for (
        let i = 0;
        i < items.length;
        i++
      ) {
    
        const [
          label,
          value
        ] = items[i];
    
    
        const row =
          column.addStack();
    
    
        row.centerAlignContent();
    
    
        const labelText =
          row.addText(label);
    
    
        labelText.font =
          Font.mediumSystemFont(
            large ? 11 : 9
          );
    
    
        labelText.textColor =
          CONFIG.SUBTEXT;
    
    
        row.addSpacer();
    
    
        const valueText =
          row.addText(
            "+" +
            value.toLocaleString(
              "ja-JP"
            )
          );
    
    
        valueText.font =
          Font.boldRoundedSystemFont(
            large ? 15 : 12
          );
    
    
        valueText.textColor =
          CONFIG.TEXT;
    
    
        if (
          i <
          items.length - 1
        ) {
    
          column.addSpacer(
            large ? 5 : 3
          );
        }
      }
    }
    
    
    // =====================================================
    // 小サイズ統計
    // 中央寄せ・縦並び
    // =====================================================
    
    function addSmallStats(
      widget,
      stats
    ) {
    
      const container =
        widget.addStack();
    
    
      container.addSpacer();
    
    
      const column =
        container.addStack();
    
    
      column.layoutVertically();
    
    
      const items = [
    
        ["今年", stats.year],
        ["今月", stats.month],
        ["今週", stats.week]
    
      ];
    
    
      for (
        let i = 0;
        i < items.length;
        i++
      ) {
    
        const [
          label,
          value
        ] = items[i];
    
    
        const row =
          column.addStack();
    
    
        row.size =
          new Size(
            86,
            0
          );
    
    
        const labelText =
          row.addText(label);
    
    
        labelText.font =
          Font.mediumSystemFont(9);
    
    
        labelText.textColor =
          CONFIG.SUBTEXT;
    
    
        row.addSpacer();
    
    
        const valueText =
          row.addText(
            "+" +
            value.toLocaleString(
              "ja-JP"
            )
          );
    
    
        valueText.font =
          Font.boldRoundedSystemFont(
            10
          );
    
    
        // 全て白
        valueText.textColor =
          CONFIG.TEXT;
    
    
        if (
          i <
          items.length - 1
        ) {
    
          column.addSpacer(1);
        }
      }
    
    
      container.addSpacer();
    }
    
    
    // =====================================================
    // 開発元
    // =====================================================
    
    function cleanOrganization(
      value
    ) {
    
      if (!value) {
    
        return "—";
      }
    
    
      return value
    
        .replace(
          /^\[|\]$/g,
          ""
        )
    
        .replace(
          /["']/g,
          ""
        )
    
        .split(",")[0]
    
        .trim();
    }
    
    
    // =====================================================
    // ○日前
    // =====================================================
    
    function relativeDate(
      value
    ) {
    
      const date =
        new Date(value);
    
    
      const now =
        new Date();
    
    
      const today =
        new Date(
          now.getFullYear(),
          now.getMonth(),
          now.getDate()
        );
    
    
      const target =
        new Date(
          date.getFullYear(),
          date.getMonth(),
          date.getDate()
        );
    
    
      const diff =
        Math.floor(
          (
            today -
            target
          ) /
          86400000
        );
    
    
      if (
        diff <= 0
      ) {
    
        return "今日";
      }
    
    
      if (
        diff === 1
      ) {
    
        return "1日前";
      }
    
    
      return `${diff}日前`;
    }
    
    
    // =====================================================
    // 中サイズ 最新AI
    // =====================================================
    
    function addMediumLatest(
      widget,
      models
    ) {
    
      const title =
        widget.addText(
          "最近公開されたAI"
        );
    
    
      title.font =
        Font.semiboldSystemFont(9);
    
    
      title.textColor =
        CONFIG.SUBTEXT;
    
    
      widget.addSpacer(4);
    
    
      const list =
        models.slice(
          0,
          3
        );
    
    
      for (
        let i = 0;
        i < list.length;
        i++
      ) {
    
        const model =
          list[i];
    
    
        const row =
          widget.addStack();
    
    
        row.centerAlignContent();
    
    
        const name =
          row.addText(
            model.name
          );
    
    
        name.font =
          Font.semiboldSystemFont(9);
    
    
        name.textColor =
          CONFIG.TEXT;
    
    
        name.lineLimit = 1;
    
    
        name.minimumScaleFactor =
          0.55;
    
    
        row.addSpacer();
    
    
        const org =
          row.addText(
            cleanOrganization(
              model.organization
            )
          );
    
    
        org.font =
          Font.mediumSystemFont(8);
    
    
        org.textColor =
          CONFIG.SUBTEXT;
    
    
        org.lineLimit = 1;
    
    
        row.addSpacer(20);
    
    
        const ago =
          row.addText(
            relativeDate(
              model.date
            )
          );
    
    
        ago.font =
          Font.mediumSystemFont(8);
    
    
        ago.textColor =
          CONFIG.ACCENT;
    
    
        ago.lineLimit = 1;
    
    
        if (
          i <
          list.length - 1
        ) {
    
          widget.addSpacer(2);
        }
      }
    }
    
    
    // =====================================================
    // 小サイズ 最新AI 1件
    // =====================================================
    
    function addSmallLatest(
      widget,
      models
    ) {
    
      if (
        !models ||
        !models.length
      ) {
    
        return;
      }
    
    
      const model =
        models[0];
    
    
      // -------------------------------------------------
      // 見出し
      // -------------------------------------------------
    
      const titleRow =
        widget.addStack();
    
    
      titleRow.addSpacer();
    
    
      const title =
        titleRow.addText(
          "最近公開されたAI"
        );
    
    
      title.font =
        Font.semiboldSystemFont(8);
    
    
      title.textColor =
        CONFIG.SUBTEXT;
    
    
      titleRow.addSpacer();
    
    
      widget.addSpacer(2);
    
    
      // -------------------------------------------------
      // AI名 + 日付
      // 行全体を中央寄せ
      // -------------------------------------------------
    
      const outer =
        widget.addStack();
    
    
      outer.addSpacer();
    
    
      const row =
        outer.addStack();
    
    
      row.centerAlignContent();
    
    
      const name =
        row.addText(
          model.name
        );
    
    
      name.font =
        Font.semiboldSystemFont(8);
    
    
      name.textColor =
        CONFIG.TEXT;
    
    
      name.lineLimit = 1;
    
    
      name.minimumScaleFactor =
        0.5;
    
    
      row.addSpacer(5);
    
    
      const ago =
        row.addText(
          relativeDate(
            model.date
          )
        );
    
    
      ago.font =
        Font.mediumSystemFont(8);
    
    
      ago.textColor =
        CONFIG.ACCENT;
    
    
      ago.lineLimit = 1;
    
    
      outer.addSpacer();
    }
    
    
    // =====================================================
    // 大サイズ 最新AI
    // =====================================================
    
    function addLargeLatest(
      widget,
      models
    ) {
    
      const title =
        widget.addText(
          "最近公開されたAI"
        );
    
    
      title.font =
        Font.semiboldSystemFont(11);
    
    
      title.textColor =
        CONFIG.SUBTEXT;
    
    
      widget.addSpacer(8);
    
    
      const list =
        models.slice(
          0,
          4
        );
    
    
      for (
        let i = 0;
        i < list.length;
        i++
      ) {
    
        const model =
          list[i];
    
    
        const row =
          widget.addStack();
    
    
        row.centerAlignContent();
    
    
        const name =
          row.addText(
            model.name
          );
    
    
        name.font =
          Font.semiboldSystemFont(12);
    
    
        name.textColor =
          CONFIG.TEXT;
    
    
        name.lineLimit = 1;
    
    
        name.minimumScaleFactor =
          0.55;
    
    
        row.addSpacer();
    
    
        const org =
          row.addText(
            cleanOrganization(
              model.organization
            )
          );
    
    
        org.font =
          Font.mediumSystemFont(10);
    
    
        org.textColor =
          CONFIG.SUBTEXT;
    
    
        org.lineLimit = 1;
    
    
        row.addSpacer(28);
    
    
        const ago =
          row.addText(
            relativeDate(
              model.date
            )
          );
    
    
        ago.font =
          Font.mediumSystemFont(9);
    
    
        ago.textColor =
          CONFIG.ACCENT;
    
    
        ago.lineLimit = 1;
    
    
        if (
          i <
          list.length - 1
        ) {
    
          widget.addSpacer(7);
        }
      }
    }
    
    
    // =====================================================
    // 大サイズ 今年の分野
    // =====================================================
    
    function addDomainSection(
      widget,
      domains
    ) {
    
      const title =
        widget.addText(
          "今年公開されたAI"
        );
    
    
      title.font =
        Font.semiboldSystemFont(11);
    
    
      title.textColor =
        CONFIG.SUBTEXT;
    
    
      widget.addSpacer(8);
    
    
      // 上位3分野
      const top =
        domains.slice(
          0,
          3
        );
    
    
      // 4位以下をOther
      const rest =
        domains.slice(3);
    
    
      const otherCount =
        rest.reduce(
          (sum, item) =>
            sum + item.count,
          0
        );
    
    
      const display =
        [...top];
    
    
      if (
        otherCount > 0
      ) {
    
        display.push({
          name: "Other",
          count: otherCount
        });
      }
    
    
      for (
        let i = 0;
        i < display.length;
        i++
      ) {
    
        const item =
          display[i];
    
    
        const row =
          widget.addStack();
    
    
        const name =
          row.addText(
            item.name
          );
    
    
        name.font =
          Font.mediumSystemFont(10);
    
    
        name.textColor =
          CONFIG.TEXT;
    
    
        name.lineLimit = 1;
    
    
        row.addSpacer();
    
    
        const count =
          row.addText(
            item.count.toLocaleString(
              "ja-JP"
            )
          );
    
    
        count.font =
          Font.boldRoundedSystemFont(
            11
          );
    
    
        count.textColor =
          CONFIG.TEXT;
    
    
        if (
          i <
          display.length - 1
        ) {
    
          widget.addSpacer(4);
        }
      }
    }
    
    
    // =====================================================
    // 小サイズ
    // =====================================================
    
    function createSmall(
      stats
    ) {
    
      const widget =
        new ListWidget();
    
    
      widget.backgroundColor =
        CONFIG.BG;
    
    
      widget.setPadding(
        12,
        12,
        11,
        12
      );
    
    
      // ヘッダー中央
      addHeader(
        widget,
        true
      );
    
    
      widget.addSpacer(6);
    
    
      // モデル数中央
      addMainNumber(
        widget,
        stats.total,
        32,
        true,
        false
      );
    
    
      widget.addSpacer(5);
    
    
      // 今年 / 今月 / 今週
      addSmallStats(
        widget,
        stats
      );
    
    
      widget.addSpacer(5);
    
    
      // 最新AI 1件
      addSmallLatest(
        widget,
        stats.latest
      );
    
    
      return widget;
    }
    
    
    // =====================================================
    // 中サイズ
    // =====================================================
    
    function createMedium(
      stats
    ) {
    
      const widget =
        new ListWidget();
    
    
      widget.backgroundColor =
        CONFIG.BG;
    
    
      widget.setPadding(
        12,
        16,
        11,
        16
      );
    
    
      addHeader(widget);
    
    
      widget.addSpacer(4);
    
    
      const top =
        widget.addStack();
    
    
      top.centerAlignContent();
    
    
      const left =
        top.addStack();
    
    
      left.layoutVertically();
    
    
      addMainNumber(
        left,
        stats.total,
        35
      );
    
    
      top.addSpacer();
    
    
      const right =
        top.addStack();
    
    
      right.size =
        new Size(
          82,
          0
        );
    
    
      right.layoutVertically();
    
    
      addVerticalStats(
        right,
        stats,
        false
      );
    
    
      widget.addSpacer(6);
    
    
      addMediumLatest(
        widget,
        stats.latest
      );
    
    
      return widget;
    }
    
    
    // =====================================================
    // 大サイズ
    // =====================================================
    
    function createLarge(
      stats
    ) {
    
      const widget =
        new ListWidget();
    
    
      widget.backgroundColor =
        CONFIG.BG;
    
    
      widget.setPadding(
        16,
        17,
        14,
        17
      );
    
    
      addHeader(widget);
    
    
      widget.addSpacer(10);
    
    
      // -------------------------------------------------
      // 上段
      // -------------------------------------------------
    
      const top =
        widget.addStack();
    
    
      top.centerAlignContent();
    
    
      const left =
        top.addStack();
    
    
      left.layoutVertically();
    
    
      addMainNumber(
        left,
        stats.total,
        45
      );
    
    
      top.addSpacer();
    
    
      const right =
        top.addStack();
    
    
      right.size =
        new Size(
          105,
          0
        );
    
    
      right.layoutVertically();
    
    
      addVerticalStats(
        right,
        stats,
        true
      );
    
    
      widget.addSpacer(17);
    
    
      // -------------------------------------------------
      // 最近公開されたAI 4件
      // -------------------------------------------------
    
      addLargeLatest(
        widget,
        stats.latest
      );
    
    
      widget.addSpacer(18);
    
    
      // -------------------------------------------------
      // 今年公開されたAI
      // -------------------------------------------------
    
      addDomainSection(
        widget,
        stats.domains || []
      );
    
    
      widget.addSpacer();
    
    
      // -------------------------------------------------
      // データ元
      // -------------------------------------------------
    
      const source =
        widget.addText(
          "Data: Epoch AI"
        );
    
    
      source.font =
        Font.systemFont(8);
    
    
      source.textColor =
        CONFIG.SUBTEXT;
    
    
      return widget;
    }
    
    
    // =====================================================
    // エラー
    // =====================================================
    
    function createErrorWidget(
      message
    ) {
    
      const widget =
        new ListWidget();
    
    
      widget.backgroundColor =
        CONFIG.BG;
    
    
      widget.setPadding(
        16,
        16,
        16,
        16
      );
    
    
      addHeader(widget);
    
    
      widget.addSpacer();
    
    
      const error =
        widget.addText(
          "データを取得できません"
        );
    
    
      error.font =
        Font.boldSystemFont(13);
    
    
      error.textColor =
        CONFIG.TEXT;
    
    
      widget.addSpacer(4);
    
    
      const detail =
        widget.addText(
          message
        );
    
    
      detail.font =
        Font.systemFont(8);
    
    
      detail.textColor =
        CONFIG.SUBTEXT;
    
    
      return widget;
    }
    
    
    // =====================================================
    // Widget生成
    // =====================================================
    
    function buildWidget(
      family,
      stats
    ) {
    
      if (
        family === "small"
      ) {
    
        return createSmall(
          stats
        );
      }
    
    
      if (
        family === "large"
      ) {
    
        return createLarge(
          stats
        );
      }
    
    
      return createMedium(
        stats
      );
    }
    
    
    // =====================================================
    // Main
    // =====================================================
    
    try {
    
      const stats =
        await getStats();
    
    
      // ===================================================
      // ホーム画面
      // ===================================================
    
      if (
        config.runsInWidget
      ) {
    
        const family =
          config.widgetFamily ||
          "medium";
    
    
        const widget =
          buildWidget(
            family,
            stats
          );
    
    
        widget.url =
          CONFIG.DATA_URL;
    
    
        widget.refreshAfterDate =
          new Date(
            Date.now() +
            CONFIG.CACHE_HOURS *
            60 *
            60 *
            1000
          );
    
    
        Script.setWidget(
          widget
        );
    
    
      // ===================================================
      // プレビュー
      // ===================================================
    
      } else {
    
        const alert =
          new Alert();
    
    
        alert.title =
          "プレビュー";
    
    
        alert.addAction(
          "小サイズ"
        );
    
    
        alert.addAction(
          "中サイズ"
        );
    
    
        alert.addAction(
          "大サイズ"
        );
    
    
        const choice =
          await alert.presentSheet();
    
    
        if (
          choice === 0
        ) {
    
          const widget =
            createSmall(
              stats
            );
    
    
          await widget.presentSmall();
    
    
        } else if (
          choice === 1
        ) {
    
          const widget =
            createMedium(
              stats
            );
    
    
          await widget.presentMedium();
    
    
        } else if (
          choice === 2
        ) {
    
          const widget =
            createLarge(
              stats
            );
    
    
          await widget.presentLarge();
        }
      }
    
    
    } catch (error) {
    
      console.error(error);
    
    
      const widget =
        createErrorWidget(
          String(
            error.message ||
            error
          )
        );
    
    
      if (
        config.runsInWidget
      ) {
    
        Script.setWidget(
          widget
        );
    
      } else {
    
        await widget.presentMedium();
      }
    }
    
    
    Script.complete();
  3. ホーム画面に追加して設定する

    iPhoneのホーム画面を長押しし、ウィジェット追加画面からScriptableを選びます。この記事の対応サイズの説明に合わせてサイズを選んで追加してください。

    追加したウィジェットを長押しして「ウィジェットを編集」を開き、「Script」に保存したスクリプトを選びます。

    「Parameter」は空欄で構いません。

    編集画面を閉じて表示を確認します。ホーム画面への反映には少し時間がかかる場合があります。

動作確認・使用結果

確認した端末 iPhone 17 ProMAX
iOS iOS27
Scriptable 1.7.19
確認したサイズ 小・中・大
バージョン 1.0.0
動作確認日 2026-10-05

表示・使用時のポイント

Epoch AIのCSVを読み、モデル名がある行を総数として集計します。日付を読める公開済みモデルについて今年・今月・直近7日間を数え、公開日が新しい順に表示します。取得結果は6時間保存し、通信失敗時は保存済みデータがあれば使います。

更新履歴

1.0.0

  • 初回リリース

よくあるエラーと対処

確認すること
表示されない場合は、ウィジェット編集画面の「Script」に保存したスクリプトが選ばれているかも確認してください。コードを貼り直すときは全文を置き換えます。
「データを取得できません」と出る
初回は通信が必要です。通信を確認してScriptableで再実行してください。
「必要な列が見つかりません」と出る
CSVのModelやPublication date列が変わった可能性があります。コード側の対応が必要です。
数値がすぐに変わらない
6時間のキャッシュがあります。CSV自体の更新とホーム画面の更新時刻にも左右されます。
分野別の合計と今年の件数が違う
1つのモデルが複数の分野に属する場合、各分野にそれぞれ数えます。

注意事項・制限

  • Epoch AIのデータセットに登録されたモデルの件数です。世界中のすべてのAIを網羅した数ではありません。
  • 「今週」はカレンダー上の月曜始まりではなく、コード上では7日前の0時以降の公開数です。
  • 総数には日付が不明な行も含まれます。期間別集計や最近公開された一覧は日付を読めた公開済みの行が対象です。
  • ホーム画面の更新タイミングはiOSが管理するため、操作やデータの変更がすぐに反映されない場合があります。

データ提供・参考資料

Epoch AI AI Models:AIモデルの公開日・開発元・分野など。
Epoch AI AI Models CSV:配布コードが集計するCSVデータ。

SHARE

この記事をシェア