kcbert-profanity / index.html
illimax's picture
Add accuracy notice
e31570c verified
Raw History Blame Contribute Delete
7.79 kB
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>kcbert-profanity</title>
<style>
:root {
--bg: #fafafa; --fg: #1a1a1a; --muted: #666; --line: #ddd;
--card: #fff; --clean: #2f7d4f; --bad: #c0392b; --btn: #1a1a1a; --btn-fg: #fff;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #141414; --fg: #eee; --muted: #999; --line: #333;
--card: #1e1e1e; --clean: #4caf7a; --bad: #e5604f; --btn: #eee; --btn-fg: #141414;
}
}
* { box-sizing: border-box; }
body {
margin: 0; background: var(--bg); color: var(--fg);
font-family: -apple-system, "Apple SD Gothic Neo", "Malgun Gothic", sans-serif;
line-height: 1.5;
}
main { max-width: 640px; margin: 0 auto; padding: 40px 16px; }
h1 { font-size: 1.5rem; margin: 0 0 4px; }
.sub { color: var(--muted); margin: 0 0 24px; font-size: .95rem; }
.sub a { color: inherit; }
textarea {
width: 100%; min-height: 96px; padding: 12px; font: inherit; resize: vertical;
border: 1px solid var(--line); border-radius: 8px; background: var(--card); color: var(--fg);
}
.row { display: flex; gap: 8px; align-items: center; margin-top: 8px; flex-wrap: wrap; }
button {
font: inherit; padding: 8px 16px; border-radius: 8px; cursor: pointer;
border: 1px solid var(--btn); background: var(--btn); color: var(--btn-fg);
}
button:disabled { opacity: .4; cursor: default; }
.ex {
background: transparent; color: var(--fg); border-color: var(--line);
padding: 4px 10px; font-size: .85rem;
}
#status { color: var(--muted); font-size: .9rem; margin-top: 16px; min-height: 1.5em; }
#result { margin-top: 8px; padding: 16px; border: 1px solid var(--line); border-radius: 8px; background: var(--card); }
#result[hidden] { display: none; }
.verdict { font-size: 1.25rem; font-weight: 600; margin-bottom: 12px; }
.bar { display: grid; grid-template-columns: 72px 1fr 56px; gap: 8px; align-items: center; font-size: .9rem; margin: 4px 0; }
.track { height: 8px; background: var(--line); border-radius: 4px; overflow: hidden; }
.fill { height: 100%; width: 0; transition: width .2s; }
.num { text-align: right; font-variant-numeric: tabular-nums; }
#words { margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--line); }
#words[hidden] { display: none; }
#words .label { color: var(--muted); font-size: .85rem; margin-bottom: 4px; }
#words mark { background: var(--bad); color: #fff; padding: 0 3px; border-radius: 3px; }
footer { margin-top: 32px; color: var(--muted); font-size: .85rem; }
</style>
</head>
<body>
<main>
<h1>kcbert-profanity</h1>
<p class="sub">ν•œκ΅­μ–΄ λ¬Έμž₯이 μš•μ„€Β·ν˜μ˜€ ν‘œν˜„μΈμ§€ νŒμ •ν•©λ‹ˆλ‹€. λͺ¨λΈ: <a href="https://huggingface.co/illimax/kcbert-profanity" target="_blank">illimax/kcbert-profanity</a></p>
<textarea id="text" placeholder="λ¬Έμž₯을 μž…λ ₯ν•˜μ„Έμš”" maxlength="500"></textarea>
<div class="row">
<button id="run" disabled>νŒμ •</button>
<button class="ex">였늘 날씨 정말 μ’‹λ‹€</button>
<button class="ex">μ•„ μ‹œλ°œ μ§„μ§œ μ§œμ¦λ‚˜λ„€</button>
<button class="ex">이 μ˜ν™” 생각보닀 λ³„λ‘œμ˜€μ–΄</button>
<button class="ex">λ„ˆ 같은 λ†ˆμ€ κ·Έλƒ₯ κΊΌμ Έ</button>
</div>
<div id="status">λͺ¨λΈ λΆˆλŸ¬μ˜€λŠ” 쀑... (처음 ν•œ 번 μ•½ 110MB)</div>
<div id="result" hidden>
<div class="verdict" id="verdict"></div>
<div class="bar"><span>정상</span><div class="track"><div class="fill" id="f0" style="background:var(--clean)"></div></div><span class="num" id="p0"></span></div>
<div class="bar"><span>μš•μ„€Β·ν˜μ˜€</span><div class="track"><div class="fill" id="f1" style="background:var(--bad)"></div></div><span class="num" id="p1"></span></div>
<div id="words" hidden>
<div class="label">νŒμ •μ— 결정적인 단어</div>
<div id="marked"></div>
</div>
</div>
<footer>μ•½ 1만 5천 λ¬Έμž₯으둜만 ν•™μŠ΅ν•œ λͺ¨λΈμ΄λΌ 잘λͺ» νŒμ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€. ν•œκ΅­μ–΄ μ „μš©μ΄λ©° μ˜μ–΄ μš•μ„€μ€ κ²€μΆœν•˜μ§€ λͺ»ν•©λ‹ˆλ‹€.<br>μž…λ ₯ν•œ λ¬Έμž₯은 μ„œλ²„λ‘œ μ „μ†‘λ˜μ§€ μ•Šκ³  λΈŒλΌμš°μ € μ•ˆμ—μ„œλ§Œ μ²˜λ¦¬λ©λ‹ˆλ‹€. 128토큰이 λ„˜λŠ” 뒷뢀뢄은 λ¬΄μ‹œλ©λ‹ˆλ‹€.</footer>
</main>
<script type="module">
import { AutoTokenizer, AutoModelForSequenceClassification } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.7.6";
const MODEL_ID = "illimax/kcbert-profanity";
const $ = (id) => document.getElementById(id);
const text = $("text"), run = $("run"), status = $("status");
let tokenizer, model;
try {
[tokenizer, model] = await Promise.all([
AutoTokenizer.from_pretrained(MODEL_ID),
AutoModelForSequenceClassification.from_pretrained(MODEL_ID, { dtype: "q8" }),
]);
status.textContent = "";
run.disabled = false;
} catch (e) {
status.textContent = "λͺ¨λΈμ„ λΆˆλŸ¬μ˜€μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€: " + e.message;
}
// λ¬Έμž₯ μ—¬λŸ¬ 개λ₯Ό ν•œ λ²ˆμ— λ„£κ³  각각의 μš•μ„€ ν™•λ₯ μ„ λŒλ €μ€€λ‹€
async function offensiveProbs(texts) {
const inputs = await tokenizer(texts, { padding: true, truncation: true, max_length: 128 });
const d = (await model(inputs)).logits.data;
return texts.map((_, i) => 1 / (1 + Math.exp(d[2 * i] - d[2 * i + 1])));
}
// λͺ¨λΈμ€ λ¬Έμž₯ λ‹¨μœ„ λΆ„λ₯˜λΌ 단어 μœ„μΉ˜λ₯Ό 직접 μ£Όμ§€ μ•ŠλŠ”λ‹€.
// κ·Έλž˜μ„œ μ–΄μ ˆμ„ ν•˜λ‚˜μ”© λΉΌ 보며 μš•μ„€ ν™•λ₯ μ„ κ°€μž₯ 크게 λ–¨μ–΄λœ¨λ¦¬λŠ” μ–΄μ ˆμ„ κ³ λ₯΄κ³ ,
// λ¬Έμž₯이 μ •μƒμœΌλ‘œ λ°”λ€” λ•ŒκΉŒμ§€ λ°˜λ³΅ν•œλ‹€. μš•μ„€μ΄ μ—¬λŸ¬ 개면 ν•˜λ‚˜λ§Œ λΉΌμ„œλŠ” ν™•λ₯ μ΄ μ•ˆ λ–¨μ–΄μ§€κΈ° λ•Œλ¬Έμ΄λ‹€.
async function findWords(words) {
const removed = new Set();
let p = 1;
while (p >= 0.5 && removed.size < words.length) {
const cand = words.map((_, i) => i).filter((i) => !removed.has(i));
const texts = cand.map((i) => words.filter((_, j) => j !== i && !removed.has(j)).join(" "));
const ps = await offensiveProbs(texts);
const best = ps.indexOf(Math.min(...ps));
removed.add(cand[best]);
p = ps[best];
}
return removed;
}
function showWords(words, removed) {
const box = $("marked");
box.replaceChildren();
words.forEach((w, i) => {
if (i) box.append(" ");
if (removed.has(i)) {
const m = document.createElement("mark");
m.textContent = w;
box.append(m);
} else {
box.append(w);
}
});
$("words").hidden = false;
}
const MAX_WORDS = 40;
async function classify() {
const value = text.value.trim();
if (!value || !model) return;
run.disabled = true;
$("words").hidden = true;
const [p1] = await offensiveProbs([value]);
const p0 = 1 - p1;
$("verdict").textContent = p1 >= 0.5 ? "μš•μ„€Β·ν˜μ˜€ ν‘œν˜„μž…λ‹ˆλ‹€" : "정상 λ¬Έμž₯μž…λ‹ˆλ‹€";
$("verdict").style.color = p1 >= 0.5 ? "var(--bad)" : "var(--clean)";
$("p0").textContent = (p0 * 100).toFixed(1) + "%";
$("p1").textContent = (p1 * 100).toFixed(1) + "%";
$("f0").style.width = p0 * 100 + "%";
$("f1").style.width = p1 * 100 + "%";
$("result").hidden = false;
const words = value.split(/\s+/);
if (p1 >= 0.5 && words.length <= MAX_WORDS) {
status.textContent = "단어 μ°ΎλŠ” 쀑...";
showWords(words, await findWords(words));
status.textContent = "";
}
run.disabled = false;
}
run.addEventListener("click", classify);
text.addEventListener("keydown", (e) => {
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); classify(); }
});
document.querySelectorAll(".ex").forEach((btn) =>
btn.addEventListener("click", () => { text.value = btn.textContent; classify(); })
);
</script>
</body>
</html>