
学生でも無料でAIを動かす方法!ブラウザだけで日本語ポジネガ判定アプリを作ろう
はじめに
今回は「アカウントもAPIキーもなしで、自分のブラウザの中で本物のAIを動かす」というテーマで解説していきます!
AIを使ってみたいと思っても、
アカウント登録が必要
APIキーやクレジットカードが必要
「13歳以上」「18歳以上」みたいな年齢の条件がある
…という壁にぶつかること、ありますよね。
この記事で作るアプリは、そのどれも使いません。HTMLファイル1つだけで、学習済みのAI(モデル)を自分のパソコンのブラウザの中で動かします。AIそのものを自分のブラウザに持ってきて動かすイメージです。
作るのは、文章を入れると「ポジティブ度」を温度計と顔で見せてくれる 「言葉の温度計AI」 です。
この記事は無料です。最後に全部のコードも載せているので、コピペでそのまま動かせます!
1. どんなアプリ?
画面はこんな感じです。

できることは4つです。
文章の判定:「ポジティブ/ニュートラル(どちらでもない)/ネガティブ」の確率を3本のバーで表示
温度計と顔:「ポジティブ度(0〜100度)」を温度計と顔で表示。ポジ寄りは赤、ネガ寄りは青
読み込みの進み具合:最初のモデルのダウンロードが何%進んだかを表示
言い換えチャレンジとAIを困らせてみよう
「今日の天気は最高!」だと 86度・😄・赤、逆に「テストが最悪だった」だと 13度・😢・青 になりました。

度数の下の「AIはこう推定しました(目安です)」は大事なので、後でもう一度話します。
2. 使っているもの
Transformers.js(@huggingface/transformers@3.8.1):ブラウザの中でAIのモデルを動かすための JavaScript のライブラリです。jsDelivr という CDN(ライブラリを配っているサイト)から読み込みます
モデル:Xenova/distilbert-base-multilingual-cased-sentiments-student
文章が「positive / neutral / negative」のどれっぽいかを判定します。日本語を含む多言語に対応
量子化版(データを小さくした版)の model_quantized.onnx(約136MB)を使います。通常版は約541MBです
バージョンは 3.8.1 で固定
// Transformers.js のCDN(バージョンは固定)
const CDN = "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.8.1";作ったときの最新版は 4 系でしたが、3 系の最新版(確認したとき)である 3.8.1 に固定しました。バージョンを書かないと、ある日ライブラリが更新されて急に動かなくなることがあるので、動作を確かめた版で固定しておくのがおすすめです。
モデルのライセンス
AIのモデルにも「どう使っていいか」のルール(ライセンス)があります。Hugging Face(AIのモデルが置いてあるサイト)のページで確かめました。
今回使う Xenova 版は、元のモデル lxyuan/distilbert-base-multilingual-cased-sentiments-student を、ブラウザで動かせる形(ONNX という、AIモデルを保存するときの形式の1つ)に変換したものです
元モデル(lxyuan 版)のライセンスは Apache-2.0 と表示されていました。Apache-2.0 は、条件(ライセンス文の表示など)を守れば使ったり配ったりできるライセンスです
Xenova 版のページには、ライセンスの表記はありませんでした(元モデルが lxyuan 版だということは書いてあります)
なので、この記事では「元モデルのライセンスは Apache-2.0」と書いておきます。ちなみに、このアプリはモデルを自分で配っているわけではありません。ページを開いた人のブラウザが、Hugging Face から直接ダウンロードする形です。
3. 動かしてみよう
準備するもの
Chrome などのブラウザ。WebAssembly(ブラウザの中でプログラムを速く動かす仕組み)で動くので、WebGPU は使いません。動作確認は Windows の Chrome 154 と Edge 155 で行いました(Playwright(自動でブラウザを操作して確かめるツール)で動かしたものも含みます)
Python(パソコンの中に小さなサーバーを立てるために使います)
インターネット(AIのモデルなどをダウンロードします)
Python が入っているかは、コマンドプロンプトで次のコマンドを打つと確かめられます。Python 3.〇〇 のようにバージョンが出ればOKです。
python --version起動のしかた
index.html を置いたフォルダをエクスプローラーで開いて、上のアドレスバーに cmd と打って Enter を押すと、そのフォルダでコマンドプロンプトが開きます。そこで次のコマンドを打ちます(Mac の人は python を python3 にしてください)。
python -m http.server 8000ブラウザで http://localhost:8000 を開けばOKです。終わったら、コマンドプロンプトで Ctrl + C を押してサーバーを止めます。
ちなみに、index.html をダブルクリックで開く(file://)方法でも、Chrome と Edge で同じように動きました!(このときは、1回目の準備が 8.5〜12秒、2回目はキャッシュが効いて 1.2秒ほど。判定も「今日の天気は最高!」が 86度、「テストが最悪だった」が 13度で、http.server で開いたときと同じでした)。学校などのネットでは、python -m http.server で開くほうが安心です。
最初だけダウンロードがある
開くと、まず「AIを読み込み中… ○%」と表示されて、モデルのダウンロードが始まります。

ここで本当にAIのモデルを自分のブラウザに持ってきています。ブラウザが受け取った量は、WebAssembly のファイルなども合わせて約141MBでした(画面の「139MB」には WebAssembly のファイルが入っていません)。
開いてから「✅ AIの準備ができました!」が出るまでの時間を測ったら、こうでした。
1回目(キャッシュなし):6.8秒
キャッシュを消してもう一度:5.9秒
2回目以降:0.43〜0.49秒
ただし、これは開発に使ったパソコンの速い回線で測った値です。遅い回線や学校のネットだと、もっと時間がかかります。学校のフィルタなどで huggingface.co や cdn.jsdelivr.net(モデル本体の実際のダウンロード元は hf.co です)がブロックされていると、つながらないこともあります(そのときは赤い文字でエラーが出ます)。
2回目以降が速いのは、モデルをブラウザがキャッシュ(保存)しておいて、そこから読むからです。ただ、Transformers.js 本体と WebAssembly のファイルは、2回目以降も CDN から取りに行っていました。なので、ネットにつながっていない状態(オフライン)では動きません。
入力した文章は外に送られる?
「どこかに送られるんじゃないの?」と気になる人もいると思います。モデルの準備ができたあと、ページから出ていく通信を全部記録しながら、お試しボタンや自分で書いた文、言い換えチャレンジ、困らせボタンで判定してみたら、判定しているあいだのリクエストは0件でした。通信があったのは読み込みのときだけで、相手は Hugging Face(モデルの取得)と jsDelivr(Transformers.js などの取得)です。
つまり「この確認の範囲では、判定するときに入力した文章はどこにも送られていなかった」ということです。ただ、確認したのはページからの通信だけで、ブラウザの拡張機能などの通信までは見ていません。
それでも、他人の悪口や、名前・学校名などの個人情報は入れないようにしましょう。入力欄にもそう書いています。
4. コードを見ていこう
コードは index.html の1ファイルだけです。大事なところを順番に見ていきます(全部のコードは記事の最後にあります)。
AIを読み込む
const { pipeline } = await import(CDN); // ここで失敗してもエラー表示できるように try の中で読み込む
classifier = await pipeline("sentiment-analysis", MODEL, { dtype: "q8", progress_callback: onProgress });pipeline("sentiment-analysis", MODEL, …) で、「文章の気持ちを判定する」AIを用意します
dtype: "q8" で量子化版を使います。実際の通信でも、取りに行ったのは model_quantized.onnx だけでした
progress_callback に、ダウンロードの進み具合を受け取る関数を渡しています
この2行は try の中にあって、失敗したら画面に赤い文字でエラーを出します(理由は6章で)
進み具合を%で出す
files[p.file] = { loaded: p.loaded, total: p.total };
const all = Object.values(files);
const loaded = all.reduce((s, f) => s + f.loaded, 0);
const total = all.reduce((s, f) => s + f.total, 0);モデル本体のほかに tokenizer.json(文章をAIが読める形に区切るための設定)なども一緒にダウンロードされるので、ファイルごとの量を全部足して%にしています。
文章を判定する
async function classify(text) {
const out = await classifier(text, { top_k: null }); // top_k: null で3つ全部の確率をもらう
const scores = { positive: 0, neutral: 0, negative: 0 };
for (const r of out) scores[r.label] = r.score;
return scores;
}オプションなしで classifier("今日の天気は最高!") とすると、一番高い1つだけ(positive の 0.833…)しか返ってきませんでした。3本のバーには3つ全部の確率がほしいので、{ top_k: null } を付けます。すると3つ全部、点数の高い順に返ってきます。
[ { "label": "positive", "score": 0.8338316679000854 },
{ "label": "negative", "score": 0.11241685599088669 },
{ "label": "neutral", "score": 0.05375150218605995 } ]label は小文字で、並び順は点数の高い順です。なので「何番目か」ではなく、label の名前で取り出しています。3つの score を足すとほぼ 1 で、これがAIの出した「確率」です。
ポジティブ度(0〜100度)にする
function toDegree(s) {
return Math.round(((s.positive - s.negative + 1) / 2) * 100);
}「ポジの確率 − ネガの確率」は −1〜1 の範囲になるので、1 を足して 2 で割り、100 をかけて 0〜100 にしています。「今日の天気は最高!」なら (0.834 − 0.112 + 1) ÷ 2 × 100 で、約86度です。
顔は faceFor という関数で、75度以上なら😄、55度以上なら🙂、45度より上なら😐、25度より上なら😟、それより下なら😢にしています。
温度計の色
const hue = deg >= 50 ? 0 : 215;
const sat = Math.abs(deg - 50) * 1.8 + 10;
$("mercury").style.height = deg + "%";
$("mercury").style.background = `hsl(${hue}, ${sat}%, 55%)`;色は hsl(色相・あざやかさ・明るさ)で決めています。50度より上なら色相 0(赤)、下なら 215(青)。50度から離れるほどあざやかさ(sat)が上がって色がこくなり、50度の近くでは灰色っぽくなります。
実は最初は「0度=青(色相 220)〜100度=赤(色相 0)」をなめらかに変える式にしていました。ところが「今日の天気は最高!」(86度)でスクショを撮ってみたら、温度計がオレンジ色(色相 約31)になっていて、「ポジティブは赤」に見えませんでした。86度もあるのにオレンジ色になったときは、あれ?ってなりました(笑)。そこで今の「上は赤、下は青」の式に変えて、86度で赤、13度で青になるのをスクショで確かめています。
言い換えチャレンジ
const before = toDegree(await classify(ODAI[odaiIndex]));
const after = show(await classify(text));
const diff = after - before;お題の文と言い換えた文の両方を判定して、度数の差を「何点アップ」として出します。
お題「宿題が多すぎて最悪」を「宿題が多いけど、終わったらきっと達成感がある」に言い換えたら、お題 9度 → 言い換え 74度 🎉 65点アップ! になりました。

お題は「別のお題」ボタンで切り替えられて、今は「宿題が多すぎて最悪」「テストが最悪だった」「雨で部活が中止になった」「テスト勉強が終わらない」の4つが入っています。
5. AIを困らせてみよう
このAIは完璧ではありません。実際にいろいろな文を判定してみたら、人が読んだときの感じ方と違う向きに判定された文がけっこうありました(数字はAIの出した確率と、アプリの温度計の度数です)。
「嫌いじゃない」→ ネガティブ 0.816、14度。「嫌い」という言葉に引っ張られたのかも?
「悪くない」→ ネガティブ 0.668、23度。こちらも「悪」に引っ張られたのかもしれません
「へえ、また雨なんだ。最高だね」(皮肉のつもりの文)→ ポジティブ 0.577、64度。「最高」という言葉のほうを見たのかも?
「朝起きるのがつらい」→ ポジティブ 0.426、ネガティブ 0.397、51度。人が読むとつらそうな文なのに、僅差でポジティブが1位
否定の文や皮肉は苦手なのかもしれません。アプリの「AIを困らせてみよう」には、この4つをボタンにして入れました。

「嫌いじゃない」は、人が読むと「どちらかというと好き」くらいに受け取れる言葉ですが、AIはネガティブ寄り・😢 と判定しました。
逆に、ちゃんと当たった例もあります。
「私はこの映画が大好きで、何度も見ます!」→ ポジティブ 0.926、95度
「ありがとう!助かったよ」→ ポジティブ 0.940、96度
AIの判定は「目安」
ここが一番大事なところです。
このアプリが出すのは「AIはこう推定した」という目安です。上のとおり、平気で外します
人の気持ちや人柄を、AIの点数で決めつけないでください
友達の書いた文や他人の悪口を判定して遊ぶのはやめましょう。いじめや悪口の判定に使うものではありません
名前・学校名などの個人情報は入れないでください
自分の書いた文で「AIはこう受け取るんだ〜」と遊ぶくらいがちょうどいいと思います!
6. 詰まったところ・変えたところ
CDN の読み込みを try の中の await import() に変えた
最初は script type="module" の一番上で、ふつうに import { pipeline } from "https://cdn.jsdelivr.net/..." と書いていました。でもこの書き方だと、CDN が読めないときにスクリプト全体が動かず、画面が「読み込み中 0%」のまま止まって、エラーも出せません。そこで、4章のように try の中で await import(CDN) を使って読み込む形に変えました。
実際に cdn.jsdelivr.net への通信をブロックして試したら、こんなエラーがちゃんと画面に出ました。
AIの読み込みに失敗しました(Failed to fetch dynamically imported module: https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.8.1)。…ほかに、判定の結果が1個しか返ってこなかったので { top_k: null } を付けたこと(4章)、温度計がオレンジ色になったので色の式を変えたこと(4章)も、作りながら直したところです。
困らせボタンとお題を、実際に判定してから入れ替えた
「AIを困らせてみよう」のボタンには、最初「つまらなくはなかった」「この映画、まじでやばい」「めっちゃしんどいけど楽しい」を入れていました。
でも実際に判定してみると、「めっちゃしんどいけど楽しい」はポジティブ 0.849 でふつうに当たってしまったので外しました。困らせるつもりの文が当たってしまうのは、ちょっと悔しいですね(笑)。代わりに、実際に外れた「嫌いじゃない」「悪くない」「朝起きるのがつらい」を入れています。
さらに、言い換えチャレンジのお題に入れていた「朝起きるのがつらい」は、AIがポジティブ寄り(51度)と判定しました。お題がもともとネガティブじゃないと「言い換えて点を上げる」遊びにならないので、お題からは外して「テストが最悪だった」に差し替えました。
予想するだけでなく、実際に判定して確かめてから入れるのが大事だなと思いました。
最後に
今回は、アカウントもAPIキーもなしで、HTMLファイル1つとブラウザだけで本物のAIを動かす方法を紹介しました!
Transformers.js を使えば、ブラウザの中でAIのモデルを動かせる
2回目からはモデルをキャッシュから読むので準備が速い(ただ、ライブラリなどはネットから取るので、オフラインでは動かない)
判定のときに入力した文章は、確認した範囲では外に送られていなかった
AIはよく外す。判定は「目安」として楽しむ
自分のブラウザでAIが動くのを見ると、ぐっと身近に感じられると思います。
最後に宿題です!動かせたら、ぜひ自分で「AIを困らせる文」を考えて試してみてください。AIがどんな言葉に引っ張られやすいのか、だんだん見えてきて面白いですよ。モデルを別のものに変えれば、ほかのこともできるはずです。ぜひ自分でも改造して、AIを動かす第一歩を踏み出しましょう!
全部のコードはこちら
ここから下は、今回作った言葉の温度計AIの全部のコードです。この記事は無料なので、誰でもコピペしてそのまま動かせます!
フォルダ構成
browser-ai-sentiment-meter/
└── index.html動かし方
好きなフォルダに index.html を作って、下のコードを貼り付けて保存する
そのフォルダでコマンドプロンプトを開いて(エクスプローラーのアドレスバーに cmd と打って Enter)、次のコマンドを打つ(Mac は python3)
python -m http.server 8000ブラウザで http://localhost:8000 を開く
「AIを読み込み中… ○%」が終わったら、文章を入れて「判定」を押す
終わったらコマンドプロンプトで Ctrl + C を押してサーバーを止める
最初の1回はモデル(約136MB)のダウンロードがあるので、インターネットにつながった状態で開いてください。huggingface.co・hf.co(モデル本体のダウンロード元)・cdn.jsdelivr.net につながらない環境では動きません。
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>言葉の温度計AI</title>
<style>
body { font-family: sans-serif; background: #f4f6fb ; color: #222 ; margin: 0; padding: 16px; }
.wrap { max-width: 980px; margin: 0 auto; display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
h1 { grid-column: 1 / -1; margin: 4px 0; font-size: 26px; }
.card { background: #fff ; border-radius: 12px; padding: 16px; box-shadow: 0 2px 6px #0001 ; }
.full { grid-column: 1 / -1; }
textarea, input { width: 100%; box-sizing: border-box; font-size: 16px; padding: 8px; border: 1px solid #ccc ; border-radius: 8px; }
button { font-size: 15px; padding: 8px 14px; margin: 6px 6px 0 0; border: none; border-radius: 8px; background: #4a6cf7 ; color: #fff ; cursor: pointer; }
button.sub { background: #e8ecff ; color: #333 ; }
button:disabled { background: #aaa ; cursor: wait; }
.progress { height: 18px; background: #e5e7eb ; border-radius: 9px; overflow: hidden; }
.progress div { height: 100%; width: 0; background: #4a6cf7 ; transition: width .2s; }
.row { display: flex; align-items: center; gap: 8px; margin: 6px 0; }
.row span:first-child { width: 110px; }
.bar { flex: 1; height: 16px; background: #eee ; border-radius: 8px; overflow: hidden; }
.bar div { height: 100%; width: 0; transition: width .4s; }
.meter { display: flex; align-items: center; gap: 20px; }
.thermo { width: 34px; height: 180px; background: #eee ; border-radius: 17px; position: relative; overflow: hidden; border: 3px solid #ccc ; }
.thermo div { position: absolute; bottom: 0; width: 100%; height: 0; transition: height .5s, background .5s; }
#face { font-size: 80px; }
#temp { font-size: 28px; font-weight: bold; }
.error { color: #c00 ; font-weight: bold; }
.note { font-size: 13px; color: #666 ; }
</style>
</head>
<body>
<div class="wrap">
<h1>🌡️ 言葉の温度計AI <span class="note">(AIはあなたのブラウザの中で動いています)</span></h1>
<!-- ③ 読み込み状況 -->
<div class="card full" id="loading">
<div id="loadText">AIを読み込み中… 0%(初回だけ約136MBをダウンロードするので時間がかかります。2回目からはキャッシュで速くなります)</div>
<div class="progress"><div id="loadBar"></div></div>
</div>
<!-- ① 判定 -->
<div class="card">
<h2>文章を判定する</h2>
<textarea id="text" rows="3" placeholder="ここに文章を入れてね(他人の悪口や名前・学校名などの個人情報は入れないでね)"></textarea>
<button id="judgeBtn" disabled>判定</button>
<div>
<button class="sub sample">今日の天気は最高!</button>
<button class="sub sample">テストが最悪だった</button>
<button class="sub sample">まあ普通かな</button>
</div>
<div style="margin-top:12px">
<div class="row"><span>ポジティブ</span><div class="bar"><div id="b-positive" style="background:#f2614b"></div></div><span id="p-positive">-</span></div>
<div class="row"><span>ニュートラル</span><div class="bar"><div id="b-neutral" style="background:#9ca3af"></div></div><span id="p-neutral">-</span></div>
<div class="row"><span>ネガティブ</span><div class="bar"><div id="b-negative" style="background:#3b82f6"></div></div><span id="p-negative">-</span></div>
</div>
</div>
<!-- ② 温度計と顔 -->
<div class="card">
<h2>ポジティブ度</h2>
<div class="meter">
<div class="thermo"><div id="mercury"></div></div>
<div><div id="face">😶</div><div id="temp">-- 度</div><div class="note">AIはこう推定しました(目安です)</div></div>
</div>
</div>
<!-- ④ 言い換えチャレンジ -->
<div class="card">
<h2>言い換えチャレンジ</h2>
<p>お題:<b id="odai">宿題が多すぎて最悪</b> <button class="sub" id="nextOdai">別のお題</button></p>
<input id="rephrase" placeholder="ポジティブに言い換えてみよう">
<button id="challengeBtn" disabled>言い換えを判定</button>
<p id="challengeResult"></p>
</div>
<!-- AIを困らせてみよう -->
<div class="card">
<h2>AIを困らせてみよう</h2>
<p class="note">皮肉や「〜じゃない」のような否定の文などで、AIはよく間違えます。押して試してみよう。</p>
<button class="sub sample">へえ、また雨なんだ。最高だね</button>
<button class="sub sample">嫌いじゃない</button>
<button class="sub sample">悪くない</button>
<button class="sub sample">朝起きるのがつらい</button>
</div>
<p class="note full">※AIの判定は目安です。人の気持ちや人柄を点数で決めつけたり、悪口の判定に使ったりしないでね。</p>
</div>
<script type="module">
// Transformers.js のCDN(バージョンは固定)
const CDN = "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.8.1";
const MODEL = "Xenova/distilbert-base-multilingual-cased-sentiments-student";
const ODAI = ["宿題が多すぎて最悪", "テストが最悪だった", "雨で部活が中止になった", "テスト勉強が終わらない"];
const $ = (id) => document.getElementById(id);
let classifier = null;
let odaiIndex = 0;
// 複数ファイルのダウンロード量を合計して%を出す
const files = {};
function onProgress(p) {
if (p.status !== "progress") return;
files[p.file] = { loaded: p.loaded, total: p.total };
const all = Object.values(files);
const loaded = all.reduce((s, f) => s + f.loaded, 0);
const total = all.reduce((s, f) => s + f.total, 0);
const pct = Math.floor((loaded / total) * 100);
$("loadBar").style.width = pct + "%";
$("loadText").textContent = `AIを読み込み中… ${pct}%(${(loaded / 1e6).toFixed(0)}MB / ${(total / 1e6).toFixed(0)}MB 初回だけ時間がかかります)`;
}
// 文章を判定して {positive, neutral, negative} の形で返す
async function classify(text) {
const out = await classifier(text, { top_k: null }); // top_k: null で3つ全部の確率をもらう
const scores = { positive: 0, neutral: 0, negative: 0 };
for (const r of out) scores[r.label] = r.score;
return scores;
}
// ポジティブ度(0〜100度)= ポジの確率 − ネガの確率 を 0〜100 に直したもの
function toDegree(s) {
return Math.round(((s.positive - s.negative + 1) / 2) * 100);
}
function faceFor(deg) {
if (deg >= 75) return "😄";
if (deg >= 55) return "🙂";
if (deg > 45) return "😐";
if (deg > 25) return "😟";
return "😢";
}
function show(s) {
for (const k of ["positive", "neutral", "negative"]) {
$("b-" + k).style.width = (s[k] * 100).toFixed(0) + "%";
$("p-" + k).textContent = (s[k] * 100).toFixed(1) + "%";
}
const deg = toDegree(s);
// 50度より上は赤、下は青。50度から離れるほど色がこくなる(真ん中は灰色)
const hue = deg >= 50 ? 0 : 215;
const sat = Math.abs(deg - 50) * 1.8 + 10;
$("mercury").style.height = deg + "%";
$("mercury").style.background = `hsl(${hue}, ${sat}%, 55%)`;
$("face").textContent = faceFor(deg);
$("temp").textContent = deg + " 度";
$("temp").style.color = `hsl(${hue}, ${sat}%, 40%)`;
return deg;
}
async function judge() {
const text = $("text").value.trim();
if (!text) return;
show(await classify(text));
}
async function challenge() {
const text = $("rephrase").value.trim();
if (!text) return;
const before = toDegree(await classify(ODAI[odaiIndex]));
const after = show(await classify(text));
const diff = after - before;
$("challengeResult").innerHTML = `お題 <b>${before}度</b> → 言い換え <b>${after}度</b> ` +
(diff > 0 ? `🎉 ${diff}点アップ!` : `😅 上がらなかった…(${diff}点)もう一回!`);
}
// ボタンの動き
$("judgeBtn").onclick = judge;
$("challengeBtn").onclick = challenge;
$("nextOdai").onclick = () => {
odaiIndex = (odaiIndex + 1) % ODAI.length;
$("odai").textContent = ODAI[odaiIndex];
$("challengeResult").textContent = "";
};
document.querySelectorAll(".sample").forEach((b) => {
b.onclick = () => { $("text").value = b.textContent; if (classifier) judge(); };
});
// モデルを読み込む(失敗したらわかりやすく表示)
try {
const { pipeline } = await import(CDN); // ここで失敗してもエラー表示できるように try の中で読み込む
classifier = await pipeline("sentiment-analysis", MODEL, { dtype: "q8", progress_callback: onProgress });
$("loadBar").style.width = "100%";
$("loadText").textContent = "✅ AIの準備ができました!文章を入れて「判定」を押してね";
$("judgeBtn").disabled = false;
$("challengeBtn").disabled = false;
} catch (e) {
console.error(e);
$("loadText").className = "error";
$("loadText").textContent = `AIの読み込みに失敗しました(${e.message})。` +
"ネットにつながっているか、学校などのフィルタで huggingface.co / cdn.jsdelivr.net がブロックされていないか確認してね。";
}
</script>
</body>
</html>