From 620c1e54273a2d055375d954d2869d53413157f5 Mon Sep 17 00:00:00 2001 From: vrubelroman Date: Wed, 1 Jul 2026 18:01:36 +0000 Subject: [PATCH] Censor profanity: "..." on screen, spoken as "beep" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New profanity.js does whole-word matching against a Russian/English root list (avoids false positives like "победа", "тебя", "сукно"). Applied once per message before it's used anywhere downstream (display, TTS, AI reply input) — screen shows "..." in place of a censored word, while the TTS text uses "бип" so it's heard as a beep instead of read literally. Co-Authored-By: Claude Sonnet 5 --- Dockerfile | 2 +- ai-prompt.txt | 5 +++-- profanity.js | 34 ++++++++++++++++++++++++++++++++++ server.js | 7 +++++-- 4 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 profanity.js diff --git a/Dockerfile b/Dockerfile index 14bc2cc..9ddc905 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ WORKDIR /app COPY package.json package-lock.json* ./ RUN npm ci --omit=dev -COPY server.js db.js ai-prompt.txt ./ +COPY server.js db.js profanity.js ai-prompt.txt ./ COPY public ./public EXPOSE 3000 diff --git a/ai-prompt.txt b/ai-prompt.txt index 9dbef3b..ebaa34d 100644 --- a/ai-prompt.txt +++ b/ai-prompt.txt @@ -1,8 +1,9 @@ Ты дружелюбный ИИ-ассистент в чате стрима. Тебе присылают сообщение зрителя, -адресованное стримеру. Отвечай коротко (1-3 предложения), живо и по делу — +адресованное стримеру. Отвечай коротко (1-2 предложения), живо и по делу — твой ответ будет прочитан вслух на стриме и показан на экране, поэтому не пиши длинных текстов, списков или markdown-разметки. При этом старайся шутить. Не допускать матных слов. Можешь дерзить в ответе. -Иногда упоминай милых котиков если в тему. +Редко упоминай милых котиков если в тему. +Защищай стримера если его хотят обидеть. diff --git a/profanity.js b/profanity.js new file mode 100644 index 0000000..e19d2e7 --- /dev/null +++ b/profanity.js @@ -0,0 +1,34 @@ +// Word-level profanity filter: matches whole words against a root list +// (so a root like "еб" only censors words that start with it, not every +// word containing that substring, e.g. "тебя", "победа"). +// Deliberately simple (no leetspeak/homoglyph handling) — extend PROFANE_ROOTS +// below if you need broader coverage. + +const PROFANE_ROOTS = [ + // Russian + 'хуй', 'хуе', 'хуё', 'хуя', 'хует', + 'пизд', + 'бля', + 'еб', 'ёб', + 'муда', 'мудак', 'мудило', + 'пидор', 'пидар', 'пидр', + 'гондон', 'гандон', + 'долбоеб', 'долбоёб', + 'сука', 'суки', 'суке', 'суку', 'суками', + // English + 'fuck', 'shit', 'bitch', 'cunt', 'dick', 'asshole', 'bastard', + 'whore', 'slut', 'nigger', 'nigga', 'faggot', 'motherfuck', +]; + +const WORD_RE = /[а-яёА-ЯЁa-zA-Z]+/g; + +function isProfaneWord(word) { + const normalized = word.toLowerCase(); + return PROFANE_ROOTS.some((root) => normalized.startsWith(root)); +} + +function censorText(text, replacement) { + return text.replace(WORD_RE, (word) => (isProfaneWord(word) ? replacement : word)); +} + +module.exports = { censorText }; diff --git a/server.js b/server.js index 3654318..9d0d74f 100644 --- a/server.js +++ b/server.js @@ -17,6 +17,7 @@ const { setViewerName, updateDisplayDuration, } = require('./db'); +const { censorText } = require('./profanity'); const PORT = process.env.PORT || 3000; const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID; @@ -274,7 +275,9 @@ io.on('connection', (socket) => { return; } - const message = text.trim().slice(0, 500); + const rawMessage = text.trim().slice(0, 500); + const message = censorText(rawMessage, '...'); + const speechMessage = censorText(rawMessage, 'бип'); const displayName = (typeof name === 'string' && name.trim().slice(0, 60)) || user.name; setViewerName(streamer.id, user.id, displayName); @@ -286,7 +289,7 @@ io.on('connection', (socket) => { const broadcastOriginal = () => { broadcastMessage( room, messageId, message, displayName, durationMs, - `Сообщение от ${displayName}. ${message}`, + `${displayName}. ${speechMessage}`, streamer.tts_voice || DEFAULT_TTS_VOICE ); };