見出し画像

Transformers.js v4 の概要

以下の記事が面白かったので、簡単にまとめました。

Transformers.js v4 Preview: Now Available on NPM!


1. はじめに

Hugging Faceが開発する JavaScript向けAIライブラリ「Transformers.js v4 Preview」が公開されました。これまでのv3系から大きく進化し、WebGPU対応やモデル追加、実行速度の向上など、多くの改善ポイントが盛り込まれています

2. Transformers.jsとは?

Transformers.js」は、元々Pythonで人気の高い機械学習ライブラリ「Huggin Face Transformers」を JavaScript(ブラウザ・Node.js)に最適化したものです。

特徴は、次のとおりです。

・ブラウザ/Node.jsでのAIモデル推論が可能
・Python版APIと似たインターフェース
・NLP・画像・音声モデル対応
・WebGPU や WASM を使って高速実行

3. v4 の主な改善ポイント

3-1. WebGPUカスタムランタイムの採用

v4では 新しい WebGPUランタイムが C++で構築され、パフォーマンスと互換性が大幅に向上しました。
これにより、

・ブラウザだけでなく Node/Bun/Deno でも GPUアクセラレーションが使える
・より多くのモデルを高速かつ効率的に実行可能

といった大きな進化を果たしています。

3-2. モノレポ化 & コード品質向上

プロジェクト構成が pnpmワークスペースベースに再構成され、コードがモジュール化されました。

これにより、

・各機能ごとに独立したパッケージが作れる
・コードの見通しが良くなり、拡張がしやすい
・モデル定義が整理され、開発の効率が向上

といったメリットがあります。

3-3. 新モデル & アーキテクチャ対応

v4では、多様な最新モデルをサポートしています。

・GPT-OSS 系モデル
・Chatterbox
・Olmo3
・FalconH1 など

新しい注意メカニズムや MoE(Mixture of Experts)などもWebGPU上で動作するようになりました。

3-4. ビルドシステムを esbuild に移行

従来の Webpack からesbuild ベースに刷新しました。
これによって、

・ビルド時間が約10倍速い
・バンドルサイズが小さくなり、読み込みが高速に
・主要なライブラリ(例:transformers.web.js)は 53% ダウン

といった効果が出ています。

3-5. Tokenizers.js として分離

これまでライブラリ内部にあった トークナイザー部分を独立パッケージ化 (@huggingface/tokenizers) しました

・gzipped 約8.8KB
・依存関係なし
・ブラウザやサーバー上でも軽量に高速トークナイズ可能

となり、より柔軟に使えるようになりました。

3-6. タイプ安全性 & 開発体験の向上

TypeScriptユーザー向けに型安全性が強化され、pipeline() の戻り値型なども 入力に応じて柔軟に推論されるようになっています。

4. まとめ

「Transformers.js v4」は、JavaScriptによるAIモデル実行の可能性を大きく広げるアップデートです。ブラウザ・Node.js両方でGPUアクセラレーションが使え、モデル対応や開発体験も進化します。今後のAI×Webアプリケーションでの活用がますます加速しそうです。

【おまけ】 Codex 製のサンプルコード

・index.html

<!doctype html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="icon" href="data:," />
    <title>Transformers.js Browser Demo</title>
    <style>
      body {
        font-family: sans-serif;
        max-width: 860px;
        margin: 32px auto;
        padding: 0 16px;
        line-height: 1.5;
      }
      button {
        padding: 8px 14px;
        font-size: 14px;
      }
      textarea {
        width: 100%;
        max-width: 100%;
        min-height: 96px;
        font-size: 14px;
        padding: 10px;
        box-sizing: border-box;
      }
      pre {
        white-space: pre-wrap;
        background: #f6f8fa;
        padding: 12px;
        border-radius: 8px;
      }
      #status {
        color: #444;
      }
    </style>
  </head>
  <body>
    <h1>Transformers.js (Browser)</h1>
    <p>初回はモデルダウンロードのため時間がかかります。</p>
    <label for="systemPrompt">システムプロンプト</label>
    <textarea id="systemPrompt">あなたは日本語のアシスタントです。常に短い文章で簡潔に返答してください。</textarea>
    <label for="prompt">ユーザーメッセージ</label>
    <textarea id="prompt">こんにちは。自己紹介してください。</textarea>
    <button id="run">生成する</button>
    <p id="status">待機中</p>
    <pre id="result"></pre>
    <script type="module" src="./browser.js"></script>
  </body>
</html>

・browser.js

import { pipeline, TextStreamer } from "./node_modules/@huggingface/transformers/dist/transformers.min.js";

const runButton = document.getElementById("run");
const statusEl = document.getElementById("status");
const resultEl = document.getElementById("result");
const promptEl = document.getElementById("prompt");
const systemPromptEl = document.getElementById("systemPrompt");

let generatorPromise = null;

function pickFinalAnswer(text) {
  if (!text) return "";

  // Handle leaked role/style wrappers like:
  // analysis ... assistantfinal ...
  const markers = ["assistantfinal", "final", "assistant\nfinal"];
  for (const marker of markers) {
    const idx = text.toLowerCase().lastIndexOf(marker.toLowerCase());
    if (idx !== -1) {
      const sliced = text.slice(idx + marker.length).trim();
      if (sliced) return sliced;
    }
  }

  return text.trim();
}

async function getGenerator() {
  if (!generatorPromise) {
    statusEl.textContent = "モデルを初期化中...";
    const onProgress = (info) => {
      if (info?.status === "progress") {
        const name = info.file ?? "model";
        const pct = Number.isFinite(info.progress) ? info.progress.toFixed(1) : "?";
        statusEl.textContent = `ダウンロード中: ${name} (${pct}%)`;
      } else if (info?.status) {
        statusEl.textContent = `準備中: ${info.status}`;
      }
    };

    generatorPromise = (async () => {
      try {
        return await pipeline("text-generation", "onnx-community/gpt-oss-20b-ONNX", {
          device: "webgpu",
          progress_callback: onProgress,
        });
      } catch {
        statusEl.textContent = "WebGPUが使えないためWASM(CPU)で初期化中...";
        return await pipeline("text-generation", "onnx-community/gpt-oss-20b-ONNX", {
          device: "wasm",
          progress_callback: onProgress,
        });
      }
    })();
  }
  return generatorPromise;
}

runButton.addEventListener("click", async () => {
  runButton.disabled = true;
  resultEl.textContent = "";
  try {
    const prompt = promptEl.value.trim();
    const systemPrompt = systemPromptEl.value.trim();
    if (!prompt) {
      statusEl.textContent = "入力してください";
      return;
    }

    const generator = await getGenerator();
    statusEl.textContent = "生成中...";
    let streamedText = "";
    const streamer = new TextStreamer(generator.tokenizer, {
      skip_prompt: true,
      skip_special_tokens: true,
      callback_function: (text) => {
        streamedText += text;
        resultEl.textContent = pickFinalAnswer(streamedText);
      },
    });

    let output;
    try {
      const messages = [];
      if (systemPrompt) messages.push({ role: "system", content: systemPrompt });
      messages.push({ role: "user", content: prompt });
      output = await generator(messages, { max_new_tokens: 512, streamer });
    } catch {
      const mergedPrompt = systemPrompt ? `${systemPrompt}\n\nユーザー: ${prompt}` : prompt;
      output = await generator(mergedPrompt, { max_new_tokens: 512, streamer });
    }

    if (!resultEl.textContent.trim()) {
      const rawText =
        output?.[0]?.generated_text?.at?.(-1)?.content ??
        output?.[0]?.generated_text ??
        "";
      resultEl.textContent = pickFinalAnswer(rawText);
    } else {
      resultEl.textContent = pickFinalAnswer(resultEl.textContent);
    }
    statusEl.textContent = "完了";
  } catch (error) {
    statusEl.textContent = "エラー";
    resultEl.textContent = String(error);
    generatorPromise = null;
  } finally {
    runButton.disabled = false;
  }
});

関連



いいなと思ったら応援しよう!