Censor profanity: "..." on screen, spoken as "beep"
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 29s
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 29s
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 <noreply@anthropic.com>
This commit is contained in:
parent
f56b829054
commit
620c1e5427
4 changed files with 43 additions and 5 deletions
|
|
@ -8,7 +8,7 @@ WORKDIR /app
|
||||||
COPY package.json package-lock.json* ./
|
COPY package.json package-lock.json* ./
|
||||||
RUN npm ci --omit=dev
|
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
|
COPY public ./public
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
Ты дружелюбный ИИ-ассистент в чате стрима. Тебе присылают сообщение зрителя,
|
Ты дружелюбный ИИ-ассистент в чате стрима. Тебе присылают сообщение зрителя,
|
||||||
адресованное стримеру. Отвечай коротко (1-3 предложения), живо и по делу —
|
адресованное стримеру. Отвечай коротко (1-2 предложения), живо и по делу —
|
||||||
твой ответ будет прочитан вслух на стриме и показан на экране, поэтому не
|
твой ответ будет прочитан вслух на стриме и показан на экране, поэтому не
|
||||||
пиши длинных текстов, списков или markdown-разметки.
|
пиши длинных текстов, списков или markdown-разметки.
|
||||||
При этом старайся шутить.
|
При этом старайся шутить.
|
||||||
Не допускать матных слов.
|
Не допускать матных слов.
|
||||||
Можешь дерзить в ответе.
|
Можешь дерзить в ответе.
|
||||||
Иногда упоминай милых котиков если в тему.
|
Редко упоминай милых котиков если в тему.
|
||||||
|
Защищай стримера если его хотят обидеть.
|
||||||
|
|
|
||||||
34
profanity.js
Normal file
34
profanity.js
Normal file
|
|
@ -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 };
|
||||||
|
|
@ -17,6 +17,7 @@ const {
|
||||||
setViewerName,
|
setViewerName,
|
||||||
updateDisplayDuration,
|
updateDisplayDuration,
|
||||||
} = require('./db');
|
} = require('./db');
|
||||||
|
const { censorText } = require('./profanity');
|
||||||
|
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
|
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
|
||||||
|
|
@ -274,7 +275,9 @@ io.on('connection', (socket) => {
|
||||||
return;
|
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;
|
const displayName = (typeof name === 'string' && name.trim().slice(0, 60)) || user.name;
|
||||||
setViewerName(streamer.id, user.id, displayName);
|
setViewerName(streamer.id, user.id, displayName);
|
||||||
|
|
||||||
|
|
@ -286,7 +289,7 @@ io.on('connection', (socket) => {
|
||||||
const broadcastOriginal = () => {
|
const broadcastOriginal = () => {
|
||||||
broadcastMessage(
|
broadcastMessage(
|
||||||
room, messageId, message, displayName, durationMs,
|
room, messageId, message, displayName, durationMs,
|
||||||
`Сообщение от ${displayName}. ${message}`,
|
`${displayName}. ${speechMessage}`,
|
||||||
streamer.tts_voice || DEFAULT_TTS_VOICE
|
streamer.tts_voice || DEFAULT_TTS_VOICE
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue