From 4d317ba1d38e8df9b470542434a3529d64fd0c76 Mon Sep 17 00:00:00 2001 From: Beda Schmid Date: Sat, 4 Jul 2026 17:40:39 -0300 Subject: [PATCH] Increase meme funniness requirements --- README.md | 1 + docker-compose.yaml | 1 + src/moderation.js | 14 +++++++++++--- tests/moderation.test.js | 13 ++++++++++--- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 80c4e32..f141da6 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ The server listens on `http://localhost:8080` by default. - `ADMIN_TOKEN`: secret review URL token. If omitted, one is generated at boot and printed in server logs. - `OPENAI_API_KEY`: enables AI upload moderation. Without it, uploads are queued for admin review. - `OPENAI_MODERATION_MODEL`: moderation vision model, default `gpt-4o-mini` +- `AUTO_APPROVE_MIN_SCORE`: minimum `MEME_CONSENSUS_SCORE` for immediate AI approval, default `80` - `TRUST_PROXY`: set to `true` when running behind a trusted reverse proxy so upload limits use `X-Forwarded-For` Uploads accept PNG and JPEG images. The server rejects files over 5 MB, any image edge over `6000px`, and images over 20 million pixels. Accepted uploads are decoded, metadata-stripped, resized so the longest edge is at most `1600px`, and stored as WebP. diff --git a/docker-compose.yaml b/docker-compose.yaml index b2e9bb3..0b45a36 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -15,6 +15,7 @@ services: ADMIN_TOKEN: ${ADMIN_TOKEN} OPENAI_API_KEY: ${OPENAI_API_KEY} OPENAI_MODERATION_MODEL: ${OPENAI_MODERATION_MODEL:-gpt-4o-mini} + AUTO_APPROVE_MIN_SCORE: ${AUTO_APPROVE_MIN_SCORE:-80} MAX_IMAGE_DIMENSION: ${MAX_IMAGE_DIMENSION:-1600} WEBP_QUALITY: ${WEBP_QUALITY:-85} volumes: diff --git a/src/moderation.js b/src/moderation.js index 92cce87..7bd813b 100644 --- a/src/moderation.js +++ b/src/moderation.js @@ -1,5 +1,5 @@ const DEFAULT_MODEL = process.env.OPENAI_MODERATION_MODEL || 'gpt-4o-mini'; -const AUTO_APPROVE_MIN_SCORE = 80; +const DEFAULT_AUTO_APPROVE_MIN_SCORE = 80; export async function moderateImage({ buffer, mime }) { if (!process.env.OPENAI_API_KEY) { @@ -59,6 +59,7 @@ export async function moderateImage({ buffer, mime }) { } function moderationPrompt() { + const autoApproveMinScore = autoApproveMinScoreValue(); return [ 'You are moderating image uploads for THE_MEME_PROTOCOL, a meme gallery.', 'This is a meme site. Images may be sarcastic, roasting, edgy, political, profane, absurd, or not PG-13.', @@ -72,7 +73,7 @@ function moderationPrompt() { 'Assign MEME_CONSENSUS_SCORE from 0-100: higher means it is legal, clearly a meme/reaction/roast/remix, visually meaningful, has a real punchline, and is suitable for this site; lower means random photo, spam, ad, QR scam, screenshot dump, unclear, generic template caption, text-only joke, weak payoff, or AI-slop.', 'Return only compact JSON with keys: decision, score, reason.', 'decision must be one of: approved, pending, rejected.', - `Use approved only when it appears legal and score is at least ${AUTO_APPROVE_MIN_SCORE}.`, + `Use approved only when it appears legal and score is at least ${autoApproveMinScore}.`, 'Use pending for ambiguity, uncertainty, low meme relevance, weak quality, generic/slop memes, or anything that needs human review.', 'Use rejected only for likely illegal content.' ].join('\n'); @@ -100,12 +101,19 @@ function normalizeDecision(raw) { ? raw.reason.trim().slice(0, 300) : 'No moderation reason supplied.'; - if (decision === 'approved' && score < AUTO_APPROVE_MIN_SCORE) { + const autoApproveMinScore = autoApproveMinScoreValue(); + if (decision === 'approved' && score < autoApproveMinScore) { return { status: 'pending', score, reason: `${reason} Low MEME_CONSENSUS_SCORE queued for review.` }; } return { status: decision, score, reason }; } +function autoApproveMinScoreValue() { + const value = Number.parseInt(process.env.AUTO_APPROVE_MIN_SCORE || '', 10); + if (!Number.isFinite(value)) return DEFAULT_AUTO_APPROVE_MIN_SCORE; + return Math.max(0, Math.min(100, value)); +} + function extractOutputText(payload) { const parts = []; for (const item of payload.output || []) { diff --git a/tests/moderation.test.js b/tests/moderation.test.js index 68cc7c9..23e019b 100644 --- a/tests/moderation.test.js +++ b/tests/moderation.test.js @@ -27,22 +27,24 @@ test('queues uploads when AI moderation is not configured', async () => { test('queues model approvals below the auto-publish quality threshold', async () => { const originalApiKey = process.env.OPENAI_API_KEY; + const originalThreshold = process.env.AUTO_APPROVE_MIN_SCORE; const originalFetch = globalThis.fetch; process.env.OPENAI_API_KEY = 'test-key'; + process.env.AUTO_APPROVE_MIN_SCORE = '90'; globalThis.fetch = async (_url, options) => { const body = JSON.parse(options.body); const prompt = body.input[0].content.find((item) => item.type === 'input_text').text; assert.match(prompt, /could the exact same caption work on 50 unrelated images/); - assert.match(prompt, /Use approved only when it appears legal and score is at least 80/); + assert.match(prompt, /Use approved only when it appears legal and score is at least 90/); return { ok: true, json: async () => ({ output_text: JSON.stringify({ decision: 'approved', - score: 79, + score: 89, reason: 'Specific joke, but not strong enough.' }) }) @@ -56,7 +58,7 @@ test('queues model approvals below the auto-publish quality threshold', async () }); assert.equal(moderation.status, 'pending'); - assert.equal(moderation.score, 79); + assert.equal(moderation.score, 89); assert.match(moderation.reason, /Low MEME_CONSENSUS_SCORE queued for review/); } finally { globalThis.fetch = originalFetch; @@ -65,5 +67,10 @@ test('queues model approvals below the auto-publish quality threshold', async () } else { process.env.OPENAI_API_KEY = originalApiKey; } + if (originalThreshold === undefined) { + delete process.env.AUTO_APPROVE_MIN_SCORE; + } else { + process.env.AUTO_APPROVE_MIN_SCORE = originalThreshold; + } } });