feat: add Groq Llama Prompt Guard moderation integration
- Updated .env.example to include Groq API configuration. - Enhanced config schema in config.ts to support Groq API keys and settings. - Implemented Groq moderation API call in indonesianTextNormalizer.ts as a fallback for badword detection. - Removed the Indonesian slang normalization function and related tests to streamline moderation logic. - Updated tests to reflect changes in moderation strategy, focusing on emoji normalization and badword detection.
This commit is contained in:
@@ -52,6 +52,11 @@ AI_LLM_MODEL=free
|
|||||||
# NVIDIA Nemotron Content Safety Configuration
|
# NVIDIA Nemotron Content Safety Configuration
|
||||||
NVIDIA_NEMOTRON_API_KEY=your_nvidia_api_key_here
|
NVIDIA_NEMOTRON_API_KEY=your_nvidia_api_key_here
|
||||||
|
|
||||||
|
# Groq Llama Prompt Guard Fallback Configuration
|
||||||
|
GROQ_API_KEY=your_groq_api_key_here
|
||||||
|
GROQ_MODERATION_MODEL=meta-llama/llama-prompt-guard-2-86m
|
||||||
|
GROQ_MODERATION_BASE_URL=https://api.groq.com/openai/v1/chat/completions
|
||||||
|
|
||||||
# Database Configuration (PostgreSQL)
|
# Database Configuration (PostgreSQL)
|
||||||
# Option 1: Use DATABASE_URL for connection string
|
# Option 1: Use DATABASE_URL for connection string
|
||||||
# DATABASE_URL=postgresql://user:password@localhost:5432/discord_bot
|
# DATABASE_URL=postgresql://user:password@localhost:5432/discord_bot
|
||||||
|
|||||||
@@ -125,6 +125,17 @@ const configSchema = z
|
|||||||
.string()
|
.string()
|
||||||
.url()
|
.url()
|
||||||
.default("https://integrate.api.nvidia.com/v1/chat/completions"),
|
.default("https://integrate.api.nvidia.com/v1/chat/completions"),
|
||||||
|
/** Groq API key for Llama Prompt Guard moderation fallback. */
|
||||||
|
GROQ_API_KEY: z.string().optional(),
|
||||||
|
/** Groq moderation model identifier. */
|
||||||
|
GROQ_MODERATION_MODEL: z
|
||||||
|
.string()
|
||||||
|
.default("meta-llama/llama-prompt-guard-2-86m"),
|
||||||
|
/** Groq API base URL. */
|
||||||
|
GROQ_MODERATION_BASE_URL: z
|
||||||
|
.string()
|
||||||
|
.url()
|
||||||
|
.default("https://api.groq.com/openai/v1/chat/completions"),
|
||||||
AUTO_DELETE_FLAGGED_ENABLED: z
|
AUTO_DELETE_FLAGGED_ENABLED: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
|
|||||||
@@ -3,12 +3,10 @@ import OpenAI from "openai";
|
|||||||
import { config } from "../config.js";
|
import { config } from "../config.js";
|
||||||
import { createChildLogger } from "../logger.js";
|
import { createChildLogger } from "../logger.js";
|
||||||
import { retryWithBackoff } from "../retry.js";
|
import { retryWithBackoff } from "../retry.js";
|
||||||
import { INDONESIAN_SLANG_LEXICON } from "./resources/indonesianSlangLexicon.js";
|
|
||||||
|
|
||||||
const log = createChildLogger("indonesianTextNormalizer");
|
const log = createChildLogger("indonesianTextNormalizer");
|
||||||
|
|
||||||
const CUSTOM_EMOJI_PATTERN = /<a?:([a-zA-Z0-9_]+):(\d+)>/g;
|
const CUSTOM_EMOJI_PATTERN = /<a?:([a-zA-Z0-9_]+):(\d+)>/g;
|
||||||
const WORD_PATTERN = /[\p{L}\p{N}_]+/gu;
|
|
||||||
|
|
||||||
/** NVIDIA content safety categories that map to offensive/badword content. */
|
/** NVIDIA content safety categories that map to offensive/badword content. */
|
||||||
const NVIDIA_BAD_CATEGORIES = new Set([
|
const NVIDIA_BAD_CATEGORIES = new Set([
|
||||||
@@ -66,6 +64,7 @@ const VALID_PRIMARY_AI_FLAGS = new Set([
|
|||||||
const BADWORD_CACHE_TTL_MS = 10 * 60 * 1000;
|
const BADWORD_CACHE_TTL_MS = 10 * 60 * 1000;
|
||||||
const NEMOTRON_RATE_LIMIT_COOLDOWN_MS = 60 * 1000;
|
const NEMOTRON_RATE_LIMIT_COOLDOWN_MS = 60 * 1000;
|
||||||
const PRIMARY_AI_RATE_LIMIT_COOLDOWN_MS = 30 * 1000;
|
const PRIMARY_AI_RATE_LIMIT_COOLDOWN_MS = 30 * 1000;
|
||||||
|
const GROQ_RATE_LIMIT_COOLDOWN_MS = 60 * 1000;
|
||||||
|
|
||||||
interface BadwordCacheEntry {
|
interface BadwordCacheEntry {
|
||||||
value: string[];
|
value: string[];
|
||||||
@@ -76,6 +75,7 @@ const badwordCache = new Map<string, BadwordCacheEntry>();
|
|||||||
const inFlightBadwordLookups = new Map<string, Promise<string[]>>();
|
const inFlightBadwordLookups = new Map<string, Promise<string[]>>();
|
||||||
let nemotronUnavailableUntil = 0;
|
let nemotronUnavailableUntil = 0;
|
||||||
let primaryAiUnavailableUntil = 0;
|
let primaryAiUnavailableUntil = 0;
|
||||||
|
let groqUnavailableUntil = 0;
|
||||||
let primaryModerationClient: OpenAI | null = null;
|
let primaryModerationClient: OpenAI | null = null;
|
||||||
|
|
||||||
export interface ModerationTextEvidence {
|
export interface ModerationTextEvidence {
|
||||||
@@ -106,22 +106,6 @@ export function normalizeDiscordCustomEmoji(text: string): {
|
|||||||
return { text: normalized, emojiNames };
|
return { text: normalized, emojiNames };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeIndonesianSlang(text: string): {
|
|
||||||
text: string;
|
|
||||||
notes: string[];
|
|
||||||
} {
|
|
||||||
const notes: string[] = [];
|
|
||||||
const normalized = text.replace(WORD_PATTERN, (word) => {
|
|
||||||
const entry = INDONESIAN_SLANG_LEXICON[word.toLowerCase()];
|
|
||||||
if (!entry) return word;
|
|
||||||
|
|
||||||
notes.push(`${word}=${entry.normalized} (${entry.note})`);
|
|
||||||
return entry.normalized;
|
|
||||||
});
|
|
||||||
|
|
||||||
return { text: normalized, notes: Array.from(new Set(notes)) };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Local fallback badword list (used when NVIDIA API is unavailable)
|
// Local fallback badword list (used when NVIDIA API is unavailable)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -372,6 +356,61 @@ async function callPrimaryAiModeration(text: string): Promise<string[]> {
|
|||||||
return extractFlagsFromPrimaryAiContent(content);
|
return extractFlagsFromPrimaryAiContent(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Groq Llama Prompt Guard Moderation API (Fallback)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call Groq Llama Prompt Guard 2-86M model for moderation scoring.
|
||||||
|
* Returns a probability score as a string (e.g. "0.9988824725151062").
|
||||||
|
* Scores above ~0.5 indicate moderation violations.
|
||||||
|
*/
|
||||||
|
async function callGrokModeration(text: string): Promise<string[]> {
|
||||||
|
const apiKey = config.GROQ_API_KEY;
|
||||||
|
if (!apiKey) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.post(
|
||||||
|
config.GROQ_MODERATION_BASE_URL,
|
||||||
|
{
|
||||||
|
model: config.GROQ_MODERATION_MODEL,
|
||||||
|
messages: [{ role: "user", content: text }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
timeout: 10_000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const scoreStr = response.data?.choices?.[0]?.message?.content?.trim();
|
||||||
|
if (!scoreStr) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the score (Llama Prompt Guard returns a single probability score)
|
||||||
|
const score = parseFloat(scoreStr);
|
||||||
|
if (isNaN(score) || score < 0.5) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map score to moderation flags based on severity
|
||||||
|
const flags: string[] = [];
|
||||||
|
if (score >= 0.9) {
|
||||||
|
flags.push("vulgar_language", "harassment");
|
||||||
|
} else if (score >= 0.7) {
|
||||||
|
flags.push("vulgar_language");
|
||||||
|
} else {
|
||||||
|
flags.push("spam");
|
||||||
|
}
|
||||||
|
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// NVIDIA Nemotron-3 Content Safety API
|
// NVIDIA Nemotron-3 Content Safety API
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -512,11 +551,35 @@ export async function detectIndonesianBadwords(
|
|||||||
}
|
}
|
||||||
log.warn(
|
log.warn(
|
||||||
{ error },
|
{ error },
|
||||||
"Primary AI badword detection failed, falling back to local detection",
|
"Primary AI badword detection failed, falling back to Groq then local detection",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try Groq Llama Prompt Guard as final API fallback before local detection.
|
||||||
|
if (hits.size === 0 && Date.now() >= groqUnavailableUntil) {
|
||||||
|
const groqKey = config.GROQ_API_KEY;
|
||||||
|
if (groqKey) {
|
||||||
|
try {
|
||||||
|
const groqHits = await callGrokModeration(text);
|
||||||
|
for (const hit of groqHits) {
|
||||||
|
hits.add(hit);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const status = axios.isAxiosError(error)
|
||||||
|
? error.response?.status
|
||||||
|
: null;
|
||||||
|
if (status === 429) {
|
||||||
|
groqUnavailableUntil = Date.now() + GROQ_RATE_LIMIT_COOLDOWN_MS;
|
||||||
|
}
|
||||||
|
log.warn(
|
||||||
|
{ error },
|
||||||
|
"Groq Llama Prompt Guard moderation failed, falling back to local detection",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const finalHits = Array.from(hits);
|
const finalHits = Array.from(hits);
|
||||||
setCachedBadwords(cacheKey, finalHits);
|
setCachedBadwords(cacheKey, finalHits);
|
||||||
return finalHits;
|
return finalHits;
|
||||||
@@ -539,9 +602,8 @@ export async function buildModerationTextEvidence(
|
|||||||
text: string,
|
text: string,
|
||||||
): Promise<ModerationTextEvidence> {
|
): Promise<ModerationTextEvidence> {
|
||||||
const emojiNormalized = normalizeDiscordCustomEmoji(text);
|
const emojiNormalized = normalizeDiscordCustomEmoji(text);
|
||||||
const slangNormalized = normalizeIndonesianSlang(emojiNormalized.text);
|
const badwordHits = await detectIndonesianBadwords(emojiNormalized.text);
|
||||||
const badwordHits = await detectIndonesianBadwords(slangNormalized.text);
|
const notes: string[] = [];
|
||||||
const notes = [...slangNormalized.notes];
|
|
||||||
|
|
||||||
for (const emojiName of emojiNormalized.emojiNames) {
|
for (const emojiName of emojiNormalized.emojiNames) {
|
||||||
notes.push(
|
notes.push(
|
||||||
@@ -557,7 +619,7 @@ export async function buildModerationTextEvidence(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
raw: text,
|
raw: text,
|
||||||
normalized: slangNormalized.text,
|
normalized: emojiNormalized.text,
|
||||||
notes: Array.from(new Set(notes)),
|
notes: Array.from(new Set(notes)),
|
||||||
badwords: badwordHits,
|
badwords: badwordHits,
|
||||||
hasBadwords: badwordHits.length > 0,
|
hasBadwords: badwordHits.length > 0,
|
||||||
|
|||||||
@@ -1,788 +0,0 @@
|
|||||||
export interface SlangLexiconEntry {
|
|
||||||
normalized: string;
|
|
||||||
note: string;
|
|
||||||
safeByDefault?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const INDONESIAN_SLANG_LEXICON: Record<string, SlangLexiconEntry> = {
|
|
||||||
// =========================================================================
|
|
||||||
// Pronouns & common abbreviations (neutral)
|
|
||||||
// =========================================================================
|
|
||||||
gw: { normalized: "gue", note: "first-person informal pronoun" },
|
|
||||||
gue: { normalized: "gue", note: "first-person informal pronoun" },
|
|
||||||
gua: { normalized: "gue", note: "first-person informal pronoun" },
|
|
||||||
lo: { normalized: "lu", note: "second-person informal pronoun" },
|
|
||||||
lu: { normalized: "lu", note: "second-person informal pronoun" },
|
|
||||||
loe: { normalized: "lu", note: "second-person informal pronoun" },
|
|
||||||
yg: { normalized: "yang", note: "common abbreviation" },
|
|
||||||
emg: { normalized: "memang", note: "common abbreviation" },
|
|
||||||
kyk: { normalized: "kayak", note: "common abbreviation" },
|
|
||||||
tdk: { normalized: "tidak", note: "common abbreviation" },
|
|
||||||
krn: { normalized: "karena", note: "common abbreviation" },
|
|
||||||
jgn: { normalized: "jangan", note: "common abbreviation" },
|
|
||||||
dh: { normalized: "sudah", note: "common abbreviation", safeByDefault: true },
|
|
||||||
udh: {
|
|
||||||
normalized: "sudah",
|
|
||||||
note: "common abbreviation",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
blm: {
|
|
||||||
normalized: "belum",
|
|
||||||
note: "common abbreviation",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
sdh: {
|
|
||||||
normalized: "sudah",
|
|
||||||
note: "common abbreviation",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
dg: {
|
|
||||||
normalized: "dengan",
|
|
||||||
note: "common abbreviation",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
dr: { normalized: "dari", note: "common abbreviation", safeByDefault: true },
|
|
||||||
dlm: {
|
|
||||||
normalized: "dalam",
|
|
||||||
note: "common abbreviation",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
gt: { normalized: "gitu", note: "common abbreviation", safeByDefault: true },
|
|
||||||
doang: {
|
|
||||||
normalized: "doang",
|
|
||||||
note: "Indonesian 'only/just'",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
si: { normalized: "si", note: "Indonesian particle", safeByDefault: true },
|
|
||||||
kah: {
|
|
||||||
normalized: "kah",
|
|
||||||
note: "Indonesian question particle",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
ku: {
|
|
||||||
normalized: "aku",
|
|
||||||
note: "first-person informal pronoun",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
mu: {
|
|
||||||
normalized: "kamu",
|
|
||||||
note: "second-person informal pronoun suffix",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
nya: {
|
|
||||||
normalized: "nya",
|
|
||||||
note: "Indonesian possessive suffix",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// Profanity — consonant-dropped / vowelless slang
|
|
||||||
// =========================================================================
|
|
||||||
ajg: { normalized: "anjing", note: "slang for 'anjing' (profanity)" },
|
|
||||||
anjg: { normalized: "anjing", note: "slang for 'anjing' (profanity)" },
|
|
||||||
njing: { normalized: "anjing", note: "slang for 'anjing' (profanity)" },
|
|
||||||
aj: {
|
|
||||||
normalized: "anjing",
|
|
||||||
note: "ultra-short slang for 'anjing' (profanity)",
|
|
||||||
},
|
|
||||||
anj: { normalized: "anjing", note: "short slang for 'anjing' (profanity)" },
|
|
||||||
anjingg: { normalized: "anjing", note: "elongated 'anjing' (profanity)" },
|
|
||||||
ajgg: {
|
|
||||||
normalized: "anjing",
|
|
||||||
note: "vowelless + elongated 'anjing' (profanity)",
|
|
||||||
},
|
|
||||||
// anjir variants
|
|
||||||
anjir: { normalized: "anjing", note: "slang for 'anjing' (profanity)" },
|
|
||||||
njir: { normalized: "anjing", note: "slang for 'anjing' (profanity)" },
|
|
||||||
njr: { normalized: "anjing", note: "vowelless 'anjir' (profanity)" },
|
|
||||||
anjay: { normalized: "anjing", note: "slang 'anjay' (profanity-adjacent)" },
|
|
||||||
bjir: {
|
|
||||||
normalized: "anjing",
|
|
||||||
note: "slang interjection 'bjir' (profanity-adjacent)",
|
|
||||||
},
|
|
||||||
bjirr: {
|
|
||||||
normalized: "anjing",
|
|
||||||
note: "elongated 'bjir' (profanity-adjacent)",
|
|
||||||
},
|
|
||||||
// bangsat variants
|
|
||||||
bgsd: { normalized: "bangsat", note: "slang for 'bangsat' (profanity)" },
|
|
||||||
bgst: { normalized: "bangsat", note: "slang for 'bangsat' (profanity)" },
|
|
||||||
bngst: { normalized: "bangsat", note: "slang for 'bangsat' (profanity)" },
|
|
||||||
bngsat: { normalized: "bangsat", note: "slang for 'bangsat' (profanity)" },
|
|
||||||
bgsat: { normalized: "bangsat", note: "slang for 'bangsat' (profanity)" },
|
|
||||||
bngstd: { normalized: "bangsat", note: "vowelless 'bangsat' (profanity)" },
|
|
||||||
// brengsek
|
|
||||||
brngsk: { normalized: "brengsek", note: "slang for 'brengsek' (profanity)" },
|
|
||||||
// bajingan
|
|
||||||
bjngn: { normalized: "bajingan", note: "slang for 'bajingan' (profanity)" },
|
|
||||||
bjgn: { normalized: "bajingan", note: "slang for 'bajingan' (profanity)" },
|
|
||||||
// kontol
|
|
||||||
kntl: { normalized: "kontol", note: "slang for 'kontol' (profanity)" },
|
|
||||||
knt: { normalized: "kontol", note: "slang for 'kontol' (profanity)" },
|
|
||||||
kontl: { normalized: "kontol", note: "slang for 'kontol' (profanity)" },
|
|
||||||
kntll: { normalized: "kontol", note: "elongated 'kontol' (profanity)" },
|
|
||||||
// memek
|
|
||||||
mmk: { normalized: "memek", note: "slang for 'memek' (profanity)" },
|
|
||||||
memk: { normalized: "memek", note: "slang for 'memek' (profanity)" },
|
|
||||||
mmkk: { normalized: "memek", note: "elongated 'memek' (profanity)" },
|
|
||||||
// tai
|
|
||||||
tae: { normalized: "tai", note: "slang for 'tai' (profanity)" },
|
|
||||||
ty: { normalized: "tai", note: "slang for 'tai' (profanity)" },
|
|
||||||
// goblok
|
|
||||||
gblk: { normalized: "goblok", note: "slang for 'goblok' (profanity)" },
|
|
||||||
goblog: { normalized: "goblok", note: "slang for 'goblok' (profanity)" },
|
|
||||||
gblkkk: { normalized: "goblok", note: "elongated 'goblok' (profanity)" },
|
|
||||||
// tolol
|
|
||||||
tlol: { normalized: "tolol", note: "slang for 'tolol' (profanity)" },
|
|
||||||
tloll: { normalized: "tolol", note: "elongated 'tolol' (profanity)" },
|
|
||||||
// bego
|
|
||||||
bgo: { normalized: "bego", note: "slang for 'bego' (profanity)" },
|
|
||||||
bgoo: { normalized: "bego", note: "elongated 'bego' (profanity)" },
|
|
||||||
// sialan
|
|
||||||
slan: { normalized: "sialan", note: "slang for 'sialan' (profanity)" },
|
|
||||||
sln: { normalized: "sialan", note: "slang for 'sialan' (profanity)" },
|
|
||||||
sialann: { normalized: "sialan", note: "elongated 'sialan' (profanity)" },
|
|
||||||
// jancuk (Javanese)
|
|
||||||
jncuk: {
|
|
||||||
normalized: "jancuk",
|
|
||||||
note: "Javanese slang for 'jancuk' (profanity)",
|
|
||||||
},
|
|
||||||
jcuk: {
|
|
||||||
normalized: "jancuk",
|
|
||||||
note: "Javanese slang for 'jancuk' (profanity)",
|
|
||||||
},
|
|
||||||
jncukk: { normalized: "jancuk", note: "elongated 'jancuk' (profanity)" },
|
|
||||||
// kampret
|
|
||||||
kmprt: { normalized: "kampret", note: "slang for 'kampret' (profanity)" },
|
|
||||||
kmpret: { normalized: "kampret", note: "slang for 'kampret' (profanity)" },
|
|
||||||
// pepek
|
|
||||||
ppk: { normalized: "pepek", note: "slang for 'pepek' (profanity)" },
|
|
||||||
// jembut
|
|
||||||
jmbt: { normalized: "jembut", note: "slang for 'jembut' (profanity)" },
|
|
||||||
// ngentot
|
|
||||||
ngntt: { normalized: "ngentot", note: "slang for 'ngentot' (profanity)" },
|
|
||||||
ngnt: { normalized: "ngentot", note: "slang for 'ngentot' (profanity)" },
|
|
||||||
// ngewe
|
|
||||||
ngw: { normalized: "ngewe", note: "slang for 'ngewe' (profanity)" },
|
|
||||||
// coli
|
|
||||||
cli: { normalized: "coli", note: "slang for 'coli' (profanity)" },
|
|
||||||
// celaka
|
|
||||||
clka: { normalized: "celaka", note: "slang for 'celaka' (profanity)" },
|
|
||||||
// laknat
|
|
||||||
lknt: { normalized: "laknat", note: "slang for 'laknat' (profanity)" },
|
|
||||||
// pantek
|
|
||||||
pntk: { normalized: "pantek", note: "slang for 'pantek' (profanity)" },
|
|
||||||
pntek: { normalized: "pantek", note: "slang for 'pantek' (profanity)" },
|
|
||||||
// entod
|
|
||||||
ntd: { normalized: "entod", note: "slang for 'entod' (profanity)" },
|
|
||||||
// Javanese insults
|
|
||||||
ndasmu: {
|
|
||||||
normalized: "ndasmu",
|
|
||||||
note: "Javanese insult 'ndasmu' (profanity)",
|
|
||||||
},
|
|
||||||
ndas: { normalized: "ndas", note: "Javanese insult 'ndas' (profanity)" },
|
|
||||||
// piyo (Javanese profanity)
|
|
||||||
piyoo: { normalized: "piyo", note: "Javanese slang 'piyo' (profanity)" },
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// Sexual deviation / prohibited identity topics
|
|
||||||
// =========================================================================
|
|
||||||
lgbt: { normalized: "LGBT", note: "sexual deviation / prohibited topic" },
|
|
||||||
lgbtq: { normalized: "LGBTQ", note: "sexual deviation / prohibited topic" },
|
|
||||||
lgbtqi: { normalized: "LGBTQI", note: "sexual deviation / prohibited topic" },
|
|
||||||
lgbtqa: { normalized: "LGBTQA", note: "sexual deviation / prohibited topic" },
|
|
||||||
lgb: { normalized: "LGBT", note: "abbreviation for LGBT / prohibited topic" },
|
|
||||||
lgbti: { normalized: "LGBTI", note: "sexual deviation / prohibited topic" },
|
|
||||||
// furry subculture
|
|
||||||
furry: { normalized: "furry", note: "furry / prohibited topic" },
|
|
||||||
furries: { normalized: "furry", note: "furry / prohibited topic" },
|
|
||||||
transfurry: {
|
|
||||||
normalized: "transfurry",
|
|
||||||
note: "transfurry / prohibited topic",
|
|
||||||
},
|
|
||||||
transfur: {
|
|
||||||
normalized: "transfurry",
|
|
||||||
note: "slang for 'transfurry' / prohibited topic",
|
|
||||||
},
|
|
||||||
protogen: {
|
|
||||||
normalized: "protogen",
|
|
||||||
note: "furry subculture / prohibited topic",
|
|
||||||
},
|
|
||||||
therian: { normalized: "therian", note: "therianthropy / prohibited topic" },
|
|
||||||
therianthropy: {
|
|
||||||
normalized: "therianthropy",
|
|
||||||
note: "therianthropy / prohibited topic",
|
|
||||||
},
|
|
||||||
otherkin: {
|
|
||||||
normalized: "otherkin",
|
|
||||||
note: "otherkin identity / prohibited topic",
|
|
||||||
},
|
|
||||||
// furry-adjacent terms
|
|
||||||
yiff: { normalized: "yiff", note: "furry sexual content / prohibited topic" },
|
|
||||||
fursona: { normalized: "fursona", note: "furry persona / prohibited topic" },
|
|
||||||
fursonas: {
|
|
||||||
normalized: "fursona",
|
|
||||||
note: "furry personas / prohibited topic",
|
|
||||||
},
|
|
||||||
fursuit: { normalized: "fursuit", note: "furry costume / prohibited topic" },
|
|
||||||
fursuits: {
|
|
||||||
normalized: "fursuit",
|
|
||||||
note: "furry costumes / prohibited topic",
|
|
||||||
},
|
|
||||||
// sexual orientation terms
|
|
||||||
gayy: { normalized: "gay", note: "elongated 'gay' / prohibited topic" },
|
|
||||||
lesbi: { normalized: "lesbian", note: "lesbian / prohibited topic" },
|
|
||||||
lesbii: {
|
|
||||||
normalized: "lesbian",
|
|
||||||
note: "slang for 'lesbian' / prohibited topic",
|
|
||||||
},
|
|
||||||
homo: { normalized: "homo", note: "homosexual slur / prohibited topic" },
|
|
||||||
waria: { normalized: "waria", note: "waria / prohibited topic" },
|
|
||||||
trans: { normalized: "transgender", note: "transgender / prohibited topic" },
|
|
||||||
nonbinary: {
|
|
||||||
normalized: "nonbinary",
|
|
||||||
note: "nonbinary identity / prohibited topic",
|
|
||||||
},
|
|
||||||
nb: {
|
|
||||||
normalized: "nonbinary",
|
|
||||||
note: "nonbinary abbreviation / prohibited topic",
|
|
||||||
},
|
|
||||||
genderfluid: {
|
|
||||||
normalized: "genderfluid",
|
|
||||||
note: "genderfluid / prohibited topic",
|
|
||||||
},
|
|
||||||
pansexual: { normalized: "pansexual", note: "pansexual / prohibited topic" },
|
|
||||||
asexual: { normalized: "asexual", note: "asexual / prohibited topic" },
|
|
||||||
ace: {
|
|
||||||
normalized: "asexual",
|
|
||||||
note: "asexual abbreviation / prohibited topic",
|
|
||||||
},
|
|
||||||
enby: { normalized: "enby", note: "NB/nonbinary slang / prohibited topic" },
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// Interjections / safe expressions (NOT profanity)
|
|
||||||
// =========================================================================
|
|
||||||
woy: {
|
|
||||||
normalized: "woy",
|
|
||||||
note: "casual Indonesian interjection/greeting; not SARA/hate/harassment by default",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
woyy: {
|
|
||||||
normalized: "woy",
|
|
||||||
note: "elongated 'woy'",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
woi: {
|
|
||||||
normalized: "woi",
|
|
||||||
note: "casual Indonesian interjection/greeting; not SARA/hate/harassment by default",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
woii: {
|
|
||||||
normalized: "woi",
|
|
||||||
note: "elongated 'woi'",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
oi: {
|
|
||||||
normalized: "oi",
|
|
||||||
note: "casual call/interjection; not offensive by default",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
hadeh: {
|
|
||||||
normalized: "hadeh",
|
|
||||||
note: "facepalm/tired expression; not offensive by default",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
hadeuh: {
|
|
||||||
normalized: "hadeh",
|
|
||||||
note: "facepalm/tired expression; not offensive by default",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
hadehh: {
|
|
||||||
normalized: "hadeh",
|
|
||||||
note: "elongated 'hadeh'",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
astaga: {
|
|
||||||
normalized: "astaga",
|
|
||||||
note: "Indonesian exclamation of surprise",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
astagfirullah: {
|
|
||||||
normalized: "astagfirullah",
|
|
||||||
note: "Islamic exclamation",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
masyaallah: {
|
|
||||||
normalized: "masyaallah",
|
|
||||||
note: "Islamic exclamation",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
allahu: {
|
|
||||||
normalized: "allahu",
|
|
||||||
note: "Islamic exclamation",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
aduh: {
|
|
||||||
normalized: "aduh",
|
|
||||||
note: "Indonesian expression of pain/surprise",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
aduhh: {
|
|
||||||
normalized: "aduh",
|
|
||||||
note: "elongated 'aduh'",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
buset: {
|
|
||||||
normalized: "buset",
|
|
||||||
note: "mild Indonesian exclamation",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
busyet: {
|
|
||||||
normalized: "buset",
|
|
||||||
note: "variant of 'buset'",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
gila: {
|
|
||||||
normalized: "gila",
|
|
||||||
note: "'crazy' — context-dependent; can be exclamation or insult",
|
|
||||||
},
|
|
||||||
gilaa: {
|
|
||||||
normalized: "gila",
|
|
||||||
note: "elongated 'gila' — context-dependent",
|
|
||||||
},
|
|
||||||
stres: {
|
|
||||||
normalized: "stres",
|
|
||||||
note: "stress expression",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
pusing: {
|
|
||||||
normalized: "pusing",
|
|
||||||
note: "dizzy/confused expression",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// Laughter expressions (safe)
|
|
||||||
// =========================================================================
|
|
||||||
wkwk: {
|
|
||||||
normalized: "wkwk",
|
|
||||||
note: "Indonesian laughter expression",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
wkwkwk: {
|
|
||||||
normalized: "wkwk",
|
|
||||||
note: "Indonesian laughter expression",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
wk: {
|
|
||||||
normalized: "wkwk",
|
|
||||||
note: "Indonesian laughter expression",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
wkwkwkwk: {
|
|
||||||
normalized: "wkwk",
|
|
||||||
note: "Indonesian laughter expression",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
lol: {
|
|
||||||
normalized: "lol",
|
|
||||||
note: "English laughter",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
lmao: {
|
|
||||||
normalized: "lmao",
|
|
||||||
note: "English laughter",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
rofl: {
|
|
||||||
normalized: "rofl",
|
|
||||||
note: "English laughter",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
xixixi: {
|
|
||||||
normalized: "xixixi",
|
|
||||||
note: "laughter expression",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
hehe: {
|
|
||||||
normalized: "hehe",
|
|
||||||
note: "laughter expression",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
hahaha: {
|
|
||||||
normalized: "hahaha",
|
|
||||||
note: "laughter expression",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// Casual Indonesian slang (safe)
|
|
||||||
// =========================================================================
|
|
||||||
santuy: {
|
|
||||||
normalized: "santai",
|
|
||||||
note: "relaxed/casual Indonesian",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
gaskeun: {
|
|
||||||
normalized: "gaskeun",
|
|
||||||
note: "Indonesian slang for 'let's go'",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
gas: {
|
|
||||||
normalized: "gas",
|
|
||||||
note: "Indonesian slang for 'go/yes'",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
baka: {
|
|
||||||
normalized: "baka",
|
|
||||||
note: "Japanese 'idiot' (anime slang)",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
kawai: {
|
|
||||||
normalized: "kawaii",
|
|
||||||
note: "Japanese 'cute' (anime slang)",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
sugoi: {
|
|
||||||
normalized: "sugoi",
|
|
||||||
note: "Japanese 'amazing' (anime slang)",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
nani: {
|
|
||||||
normalized: "nani",
|
|
||||||
note: "Japanese 'what' (anime slang)",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
bts: {
|
|
||||||
normalized: "bts",
|
|
||||||
note: "K-pop group abbreviation",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
otw: {
|
|
||||||
normalized: "on the way",
|
|
||||||
note: "common internet abbreviation",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
brb: {
|
|
||||||
normalized: "be right back",
|
|
||||||
note: "common internet abbreviation",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
afk: {
|
|
||||||
normalized: "away from keyboard",
|
|
||||||
note: "common internet abbreviation",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
btw: {
|
|
||||||
normalized: "by the way",
|
|
||||||
note: "English common abbreviation",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
omg: {
|
|
||||||
normalized: "oh my god",
|
|
||||||
note: "English interjection",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
pls: {
|
|
||||||
normalized: "please",
|
|
||||||
note: "English common abbreviation",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
thx: {
|
|
||||||
normalized: "thanks",
|
|
||||||
note: "English common abbreviation",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
tq: {
|
|
||||||
normalized: "thanks",
|
|
||||||
note: "English common abbreviation",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
imo: {
|
|
||||||
normalized: "in my opinion",
|
|
||||||
note: "English common abbreviation",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
tbh: {
|
|
||||||
normalized: "to be honest",
|
|
||||||
note: "English common abbreviation",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
idk: {
|
|
||||||
normalized: "I don't know",
|
|
||||||
note: "English common abbreviation",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
rn: {
|
|
||||||
normalized: "right now",
|
|
||||||
note: "English common abbreviation",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
sm: {
|
|
||||||
normalized: "sama",
|
|
||||||
note: "Indonesian 'same' or 'with'",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
gpp: {
|
|
||||||
normalized: "gapapa",
|
|
||||||
note: "Indonesian 'it's okay'",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
gapapa: {
|
|
||||||
normalized: "gapapa",
|
|
||||||
note: "Indonesian 'it's okay'",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
// =========================================================================
|
|
||||||
// Spam / scam / self-promo indicators
|
|
||||||
// =========================================================================
|
|
||||||
gcash: { normalized: "gcash", note: "potential scam/crypto term" },
|
|
||||||
airdrop: { normalized: "airdrop", note: "potential crypto scam" },
|
|
||||||
giveaway: { normalized: "giveaway", note: "potential spam — check context" },
|
|
||||||
follow4follow: { normalized: "follow4follow", note: "spam engagement" },
|
|
||||||
f4f: { normalized: "follow4follow", note: "spam engagement abbreviation" },
|
|
||||||
sub4sub: { normalized: "sub4sub", note: "spam engagement" },
|
|
||||||
s4s: { normalized: "sub4sub", note: "spam engagement abbreviation" },
|
|
||||||
like4like: { normalized: "like4like", note: "spam engagement" },
|
|
||||||
l4l: { normalized: "like4like", note: "spam engagement abbreviation" },
|
|
||||||
dm: {
|
|
||||||
normalized: "DM",
|
|
||||||
note: "direct message — check for spam/scam context",
|
|
||||||
},
|
|
||||||
pm: {
|
|
||||||
normalized: "PM",
|
|
||||||
note: "private message — check for spam/scam context",
|
|
||||||
},
|
|
||||||
click: { normalized: "click", note: "potential clickbait/scam" },
|
|
||||||
link: { normalized: "link", note: "potential spam link — check context" },
|
|
||||||
free: { normalized: "free", note: "potential spam bait — check context" },
|
|
||||||
nitro: {
|
|
||||||
normalized: "nitro",
|
|
||||||
note: "Discord Nitro — common in scam/free nitro spam",
|
|
||||||
},
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// Drug / substance slang
|
|
||||||
// =========================================================================
|
|
||||||
ganja: { normalized: "ganja", note: "marijuana / prohibited topic" },
|
|
||||||
weed: { normalized: "ganja", note: "marijuana slang / prohibited topic" },
|
|
||||||
sabu: { normalized: "sabu", note: "methamphetamine / prohibited topic" },
|
|
||||||
narkotika: { normalized: "narkotika", note: "narcotics / prohibited topic" },
|
|
||||||
narkoba: { normalized: "narkoba", note: "narcotics / prohibited topic" },
|
|
||||||
kokain: { normalized: "kokain", note: "cocaine / prohibited topic" },
|
|
||||||
ekstasi: { normalized: "ekstasi", note: "ecstasy / prohibited topic" },
|
|
||||||
shabu: {
|
|
||||||
normalized: "sabu",
|
|
||||||
note: "variant spelling 'sabu' / prohibited topic",
|
|
||||||
},
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// Violence / threat indicators
|
|
||||||
// =========================================================================
|
|
||||||
bunuh: { normalized: "bunuh", note: "kill / violence indicator" },
|
|
||||||
bunuhdiri: {
|
|
||||||
normalized: "bunuh diri",
|
|
||||||
note: "suicide / self-harm indicator",
|
|
||||||
},
|
|
||||||
mati: { normalized: "mati", note: "die / death — context-dependent" },
|
|
||||||
matiin: {
|
|
||||||
normalized: "matikan",
|
|
||||||
note: "turn off / kill — context-dependent",
|
|
||||||
},
|
|
||||||
ancam: { normalized: "ancam", note: "threat indicator" },
|
|
||||||
ancamn: { normalized: "ancaman", note: "threat indicator" },
|
|
||||||
bakar: { normalized: "bakar", note: "burn / violence indicator" },
|
|
||||||
pukul: { normalized: "pukul", note: "hit/punch / violence indicator" },
|
|
||||||
tikam: { normalized: "tikam", note: "stab / violence indicator" },
|
|
||||||
tembak: { normalized: "tembak", note: "shoot / violence indicator" },
|
|
||||||
bom: { normalized: "bom", note: "bomb / violence indicator" },
|
|
||||||
teror: { normalized: "teror", note: "terror / violence indicator" },
|
|
||||||
perang: { normalized: "perang", note: "war / conflict indicator" },
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// Religious / cultural sensitivity terms (context-dependent, not flagged by
|
|
||||||
// default — LLM must evaluate context carefully)
|
|
||||||
// =========================================================================
|
|
||||||
kafir: { normalized: "kafir", note: "religious slur — context-dependent" },
|
|
||||||
musrik: { normalized: "musyrik", note: "religious term — context-dependent" },
|
|
||||||
kufur: { normalized: "kufur", note: "religious term — context-dependent" },
|
|
||||||
sesat: { normalized: "sesat", note: "heretic — context-dependent" },
|
|
||||||
halal: {
|
|
||||||
normalized: "halal",
|
|
||||||
note: "Islamic term",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
haram: {
|
|
||||||
normalized: "haram",
|
|
||||||
note: "Islamic prohibition term — context-dependent",
|
|
||||||
},
|
|
||||||
dosa: {
|
|
||||||
normalized: "dosa",
|
|
||||||
note: "sin — context-dependent",
|
|
||||||
},
|
|
||||||
taubat: {
|
|
||||||
normalized: "taubat",
|
|
||||||
note: "repentance — context-dependent",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
shalat: {
|
|
||||||
normalized: "shalat",
|
|
||||||
note: "Islamic prayer",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
sholat: {
|
|
||||||
normalized: "sholat",
|
|
||||||
note: "Islamic prayer (variant)",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
puasa: {
|
|
||||||
normalized: "puasa",
|
|
||||||
note: "fasting",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
ramadhan: {
|
|
||||||
normalized: "ramadhan",
|
|
||||||
note: "Ramadan",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
idul: {
|
|
||||||
normalized: "idul",
|
|
||||||
note: "Islamic holiday prefix",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
fitri: {
|
|
||||||
normalized: "fitri",
|
|
||||||
note: "Islamic holiday suffix",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
qurban: {
|
|
||||||
normalized: "qurban",
|
|
||||||
note: "Islamic sacrifice",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
kurban: {
|
|
||||||
normalized: "kurban",
|
|
||||||
note: "Islamic sacrifice (variant)",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
masjid: {
|
|
||||||
normalized: "masjid",
|
|
||||||
note: "mosque",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
gereja: {
|
|
||||||
normalized: "gereja",
|
|
||||||
note: "church",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
vihara: {
|
|
||||||
normalized: "vihara",
|
|
||||||
note: "Buddhist temple",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
pura: {
|
|
||||||
normalized: "pura",
|
|
||||||
note: "Hindu temple",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
klenteng: {
|
|
||||||
normalized: "klenteng",
|
|
||||||
note: "Chinese temple",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// NSFW / sexual content terms (outside furry/LGBT scope)
|
|
||||||
// =========================================================================
|
|
||||||
hentai: { normalized: "hentai", note: "anime porn / NSFW content" },
|
|
||||||
porn: { normalized: "porno", note: "pornography / NSFW content" },
|
|
||||||
porno: { normalized: "porno", note: "pornography / NSFW content" },
|
|
||||||
bokep: { normalized: "bokep", note: "pornography / NSFW content" },
|
|
||||||
bokap: {
|
|
||||||
normalized: "bokap",
|
|
||||||
note: "'father' slang — NOT bokep",
|
|
||||||
safeByDefault: true,
|
|
||||||
},
|
|
||||||
ngeseks: { normalized: "ngeseks", note: "having sex / NSFW content" },
|
|
||||||
masturbasi: { normalized: "masturbasi", note: "masturbation / NSFW content" },
|
|
||||||
onani: { normalized: "onani", note: "masturbation / NSFW content" },
|
|
||||||
colok: { normalized: "colok", note: "sexual act / NSFW context" },
|
|
||||||
blowjob: { normalized: "blowjob", note: "sexual act / NSFW content" },
|
|
||||||
bj: { normalized: "blowjob", note: "sexual act abbreviation / NSFW content" },
|
|
||||||
cumshot: { normalized: "cumshot", note: "sexual act / NSFW content" },
|
|
||||||
creampie: { normalized: "creampie", note: "sexual act / NSFW content" },
|
|
||||||
threesome: { normalized: "threesome", note: "sexual act / NSFW content" },
|
|
||||||
orgy: { normalized: "orgy", note: "sexual act / NSFW content" },
|
|
||||||
gangbang: { normalized: "gangbang", note: "sexual act / NSFW content" },
|
|
||||||
milf: { normalized: "milf", note: "sexual category / NSFW content" },
|
|
||||||
loli: { normalized: "loli", note: "underage sexual content / illegal" },
|
|
||||||
shota: { normalized: "shota", note: "underage sexual content / illegal" },
|
|
||||||
cp: { normalized: "cp", note: "child pornography abbreviation / illegal" },
|
|
||||||
pedo: { normalized: "pedo", note: "pedophilia / illegal" },
|
|
||||||
pedofil: { normalized: "pedofil", note: "pedophile / illegal" },
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// Gambling
|
|
||||||
// =========================================================================
|
|
||||||
judi: { normalized: "judi", note: "gambling / prohibited topic" },
|
|
||||||
slot: { normalized: "slot", note: "slot gambling — check context" },
|
|
||||||
slotgacor: { normalized: "slot gacor", note: "gambling spam term" },
|
|
||||||
gacor: {
|
|
||||||
normalized: "gacor",
|
|
||||||
note: "gambling spam term — but also slang for 'good', context-dependent",
|
|
||||||
},
|
|
||||||
togel: { normalized: "togel", note: "lottery gambling / prohibited topic" },
|
|
||||||
poker: { normalized: "poker", note: "poker gambling — check context" },
|
|
||||||
sbobet: { normalized: "sbobet", note: "gambling site / prohibited topic" },
|
|
||||||
parlay: { normalized: "parlay", note: "gambling term / prohibited topic" },
|
|
||||||
maxwin: { normalized: "maxwin", note: "gambling spam term" },
|
|
||||||
rtp: { normalized: "rtp", note: "gambling RTP term — check context" },
|
|
||||||
depo: {
|
|
||||||
normalized: "deposit",
|
|
||||||
note: "deposit — common in gambling spam, check context",
|
|
||||||
},
|
|
||||||
wd: { normalized: "withdraw", note: "withdraw — common in gambling spam" },
|
|
||||||
jackpot: {
|
|
||||||
normalized: "jackpot",
|
|
||||||
note: "jackpot — common in gambling spam, check context",
|
|
||||||
},
|
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// Money / financial scam indicators
|
|
||||||
// =========================================================================
|
|
||||||
investment: {
|
|
||||||
normalized: "investment",
|
|
||||||
note: "potential investment scam — check context",
|
|
||||||
},
|
|
||||||
investasi: {
|
|
||||||
normalized: "investasi",
|
|
||||||
note: "potential investment scam — check context",
|
|
||||||
},
|
|
||||||
crypto: { normalized: "crypto", note: "crypto scam — check context" },
|
|
||||||
bitcoin: { normalized: "bitcoin", note: "crypto — check context" },
|
|
||||||
btc: { normalized: "bitcoin", note: "crypto — check context" },
|
|
||||||
usdt: { normalized: "usdt", note: "crypto stablecoin — check context" },
|
|
||||||
binance: { normalized: "binance", note: "crypto exchange — check context" },
|
|
||||||
forex: { normalized: "forex", note: "forex trading — check context" },
|
|
||||||
trading: { normalized: "trading", note: "trading — check context" },
|
|
||||||
profit: { normalized: "profit", note: "scam bait — check context" },
|
|
||||||
bonus: { normalized: "bonus", note: "spam bait — check context" },
|
|
||||||
hadiah: { normalized: "hadiah", note: "prize scam — check context" },
|
|
||||||
menang: { normalized: "menang", note: "win — check context" },
|
|
||||||
claim: { normalized: "claim", note: "scam bait — check context" },
|
|
||||||
klaim: { normalized: "klaim", note: "scam bait — check context" },
|
|
||||||
verify: { normalized: "verify", note: "potential phishing — check context" },
|
|
||||||
verifikasi: {
|
|
||||||
normalized: "verifikasi",
|
|
||||||
note: "potential phishing — check context",
|
|
||||||
},
|
|
||||||
wallet: { normalized: "wallet", note: "crypto wallet — check context" },
|
|
||||||
seed: {
|
|
||||||
normalized: "seed phrase",
|
|
||||||
note: "crypto seed phrase — check context",
|
|
||||||
},
|
|
||||||
recovery: {
|
|
||||||
normalized: "recovery phrase",
|
|
||||||
note: "crypto recovery — check context",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -5,15 +5,16 @@ import {
|
|||||||
detectIndonesianBadwords,
|
detectIndonesianBadwords,
|
||||||
formatModerationTextEvidenceForPrompt,
|
formatModerationTextEvidenceForPrompt,
|
||||||
normalizeDiscordCustomEmoji,
|
normalizeDiscordCustomEmoji,
|
||||||
normalizeIndonesianSlang,
|
|
||||||
} from "../../src/moderation/indonesianTextNormalizer";
|
} from "../../src/moderation/indonesianTextNormalizer";
|
||||||
|
|
||||||
const originalNemotronKey = config.NVIDIA_NEMOTRON_API_KEY;
|
const originalNemotronKey = config.NVIDIA_NEMOTRON_API_KEY;
|
||||||
const originalPrimaryAiKey = config.AI_LLM_API_KEY;
|
const originalPrimaryAiKey = config.AI_LLM_API_KEY;
|
||||||
|
const originalGroqKey = config.GROQ_API_KEY;
|
||||||
|
|
||||||
function disableRemoteModeration(): void {
|
function disableRemoteModeration(): void {
|
||||||
config.NVIDIA_NEMOTRON_API_KEY = undefined;
|
config.NVIDIA_NEMOTRON_API_KEY = undefined;
|
||||||
config.AI_LLM_API_KEY = undefined;
|
config.AI_LLM_API_KEY = undefined;
|
||||||
|
config.GROQ_API_KEY = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
disableRemoteModeration();
|
disableRemoteModeration();
|
||||||
@@ -25,6 +26,7 @@ afterEach(() => {
|
|||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
config.NVIDIA_NEMOTRON_API_KEY = originalNemotronKey;
|
config.NVIDIA_NEMOTRON_API_KEY = originalNemotronKey;
|
||||||
config.AI_LLM_API_KEY = originalPrimaryAiKey;
|
config.AI_LLM_API_KEY = originalPrimaryAiKey;
|
||||||
|
config.GROQ_API_KEY = originalGroqKey;
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("normalizeDiscordCustomEmoji", () => {
|
describe("normalizeDiscordCustomEmoji", () => {
|
||||||
@@ -49,20 +51,6 @@ describe("normalizeDiscordCustomEmoji", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("normalizeIndonesianSlang", () => {
|
|
||||||
it("maps common slang to normalized form", () => {
|
|
||||||
const { text, notes } = normalizeIndonesianSlang("gw emg kyk gitu");
|
|
||||||
expect(text).toBe("gue memang kayak gitu");
|
|
||||||
expect(notes.some((n) => n.startsWith("gw="))).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("marks woy as safe casual interjection", () => {
|
|
||||||
const { text, notes } = normalizeIndonesianSlang("woy hadeh");
|
|
||||||
expect(text).toBe("woy hadeh");
|
|
||||||
expect(notes.some((n) => n.includes("casual"))).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("detectIndonesianBadwords", () => {
|
describe("detectIndonesianBadwords", () => {
|
||||||
it("detects known badword via local fallback", async () => {
|
it("detects known badword via local fallback", async () => {
|
||||||
const badwords = await detectIndonesianBadwords("kontol banget");
|
const badwords = await detectIndonesianBadwords("kontol banget");
|
||||||
@@ -81,10 +69,8 @@ describe("buildModerationTextEvidence", () => {
|
|||||||
"Bersiaplah woy <:hadeh:1217434294281048185>",
|
"Bersiaplah woy <:hadeh:1217434294281048185>",
|
||||||
);
|
);
|
||||||
expect(evidence.normalized).toContain("[emoji:hadeh]");
|
expect(evidence.normalized).toContain("[emoji:hadeh]");
|
||||||
// NVIDIA API may detect "vulgar_language" for certain inputs;
|
// Slang normalization removed; only emoji normalization and badword detection remain
|
||||||
// just verify the local slang normalizer (woy, hadeh) still works
|
|
||||||
expect(evidence.notes.some((n) => n.includes("emoji:hadeh"))).toBe(true);
|
expect(evidence.notes.some((n) => n.includes("emoji:hadeh"))).toBe(true);
|
||||||
expect(evidence.notes.some((n) => n.includes("casual"))).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("detects badword when present", async () => {
|
it("detects badword when present", async () => {
|
||||||
@@ -97,7 +83,7 @@ describe("buildModerationTextEvidence", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("formatModerationTextEvidenceForPrompt", () => {
|
describe("formatModerationTextEvidenceForPrompt", () => {
|
||||||
it("returns prompt evidence for slang + emoji", async () => {
|
it("returns prompt evidence for emoji", async () => {
|
||||||
const formatted = await formatModerationTextEvidenceForPrompt(
|
const formatted = await formatModerationTextEvidenceForPrompt(
|
||||||
"Bersiaplah woy <:hadeh:1217434294281048185>",
|
"Bersiaplah woy <:hadeh:1217434294281048185>",
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user