feat(moderation): Indonesian slang normalizer and false-positive prevention
- Add indonesian-badwords dependency for local lexical signal - Add Indonesian slang lexicon with woy/woi/hadeh as safe casual terms - Normalize Discord custom emoji <:name:id> to [emoji:name] in prompts - Wire normalization evidence into both conversationContext and llmModerationClient prompts - Harden system prompt: woy/woi are casual greetings, not SARA/hate - Add tests for emoji normalization, slang mapping, badword detection Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
87a4afec26
commit
fb09ac81c5
@@ -0,0 +1,107 @@
|
||||
import badwordsModule from "indonesian-badwords";
|
||||
import { INDONESIAN_SLANG_LEXICON } from "./resources/indonesianSlangLexicon.js";
|
||||
|
||||
const CUSTOM_EMOJI_PATTERN = /<a?:([a-zA-Z0-9_]+):(\d+)>/g;
|
||||
const WORD_PATTERN = /[\p{L}\p{N}_]+/gu;
|
||||
|
||||
interface BadwordAnalyzeResult {
|
||||
badwords?: string[];
|
||||
count?: number;
|
||||
}
|
||||
|
||||
interface BadwordsModule {
|
||||
analyze?: (text: string) => BadwordAnalyzeResult;
|
||||
flag?: (text: string) => boolean;
|
||||
}
|
||||
|
||||
const badwords = badwordsModule as BadwordsModule;
|
||||
|
||||
export interface ModerationTextEvidence {
|
||||
raw: string;
|
||||
normalized: string;
|
||||
notes: string[];
|
||||
badwords: string[];
|
||||
hasBadwords: boolean;
|
||||
}
|
||||
|
||||
export function normalizeDiscordCustomEmoji(text: string): {
|
||||
text: string;
|
||||
emojiNames: string[];
|
||||
} {
|
||||
const emojiNames: string[] = [];
|
||||
const normalized = text.replace(CUSTOM_EMOJI_PATTERN, (_match, name: string) => {
|
||||
emojiNames.push(name);
|
||||
return `[emoji:${name}]`;
|
||||
});
|
||||
|
||||
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)) };
|
||||
}
|
||||
|
||||
export function detectIndonesianBadwords(text: string): string[] {
|
||||
try {
|
||||
const result = badwords.analyze?.(text);
|
||||
if (Array.isArray(result?.badwords)) {
|
||||
return Array.from(new Set(result.badwords.map((word) => word.toLowerCase())));
|
||||
}
|
||||
} catch {
|
||||
// Keep moderation pipeline resilient if dependency changes shape.
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function buildModerationTextEvidence(text: string): ModerationTextEvidence {
|
||||
const emojiNormalized = normalizeDiscordCustomEmoji(text);
|
||||
const slangNormalized = normalizeIndonesianSlang(emojiNormalized.text);
|
||||
const badwordHits = detectIndonesianBadwords(slangNormalized.text);
|
||||
const notes = [...slangNormalized.notes];
|
||||
|
||||
for (const emojiName of emojiNormalized.emojiNames) {
|
||||
notes.push(
|
||||
`emoji:${emojiName}=Discord custom emoji/expression; not text offense by default`,
|
||||
);
|
||||
}
|
||||
|
||||
if (badwordHits.length > 0) {
|
||||
notes.push(`local lexical check: Indonesian badword detected: ${badwordHits.join(", ")}`);
|
||||
} else {
|
||||
notes.push("local lexical check: no Indonesian badword detected");
|
||||
}
|
||||
|
||||
return {
|
||||
raw: text,
|
||||
normalized: slangNormalized.text,
|
||||
notes: Array.from(new Set(notes)),
|
||||
badwords: badwordHits,
|
||||
hasBadwords: badwordHits.length > 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function formatModerationTextEvidenceForPrompt(text: string): string {
|
||||
const evidence = buildModerationTextEvidence(text);
|
||||
if (evidence.normalized === evidence.raw && evidence.notes.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return [
|
||||
`[normalized_text: ${evidence.normalized}]`,
|
||||
evidence.notes.length > 0 ? `[normalization_notes: ${evidence.notes.join("; ")}]` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
}
|
||||
Reference in New Issue
Block a user