Increase meme funniness requirements
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
+11
-3
@@ -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 || []) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user