allow disproportionate memes
This commit is contained in:
@@ -22,7 +22,7 @@ The server listens on `http://localhost:8080` by default.
|
||||
- `OPENAI_MODERATION_MODEL`: moderation vision model, default `gpt-4o-mini`
|
||||
- `TRUST_PROXY`: set to `true` when running behind a trusted reverse proxy so upload limits use `X-Forwarded-For`
|
||||
|
||||
Uploads accept only square PNG and JPEG images. The server rejects files over 5 MB, images over `6000x6000`, and images over 20 million pixels. Accepted uploads are decoded, metadata-stripped, resized down to `1600x1600` if needed, and stored as WebP.
|
||||
Uploads accept PNG and JPEG images. The server rejects files over 5 MB, images over `6000x6000`, 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.
|
||||
Upload caps are 5 per hour per IP, 10 per day per IP, and 100 globally per day. AI-approved uploads publish immediately; ambiguous uploads are queued for the secret admin review page; likely illegal uploads are rejected immediately.
|
||||
|
||||
Files are stored under sharded date/hash paths:
|
||||
|
||||
@@ -193,7 +193,6 @@ async function validateClientFile(file) {
|
||||
|
||||
try {
|
||||
const dimensions = await readImageDimensions(file);
|
||||
if (dimensions.width !== dimensions.height) return 'IMAGE MUST BE SQUARE.';
|
||||
if (dimensions.width > 6000 || dimensions.height > 6000) return 'IMAGE EXCEEDS 6000x6000.';
|
||||
} catch {
|
||||
return 'IMAGE COULD NOT BE INSPECTED.';
|
||||
|
||||
@@ -296,6 +296,7 @@ a {
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
opacity: 0.9;
|
||||
transition: transform 700ms ease, opacity 200ms ease;
|
||||
}
|
||||
@@ -632,8 +633,10 @@ dialog::backdrop {
|
||||
|
||||
.lightbox img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
max-height: calc(100vh - 144px);
|
||||
margin: 0 auto;
|
||||
object-fit: contain;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
@@ -163,8 +163,7 @@ const server = http.createServer(async (req, res) => {
|
||||
maxBytes: UPLOAD_MAX_BYTES,
|
||||
maxWidth: 6000,
|
||||
maxHeight: 6000,
|
||||
maxPixels: 20_000_000,
|
||||
requireSquare: true
|
||||
maxPixels: 20_000_000
|
||||
});
|
||||
const normalized = await normalizeToWebp(upload.buffer);
|
||||
const moderation = await moderateImage({ buffer: normalized.buffer, mime: normalized.image.mime });
|
||||
|
||||
+9
-12
@@ -19,32 +19,29 @@ export async function normalizeToWebp(buffer) {
|
||||
if (!metadata?.width || !metadata?.height) {
|
||||
throw new HttpError(400, 'Image could not be decoded.');
|
||||
}
|
||||
if (metadata.width !== metadata.height) {
|
||||
throw new HttpError(422, 'Image must be square.');
|
||||
}
|
||||
|
||||
const targetSize = Math.min(metadata.width, MAX_OUTPUT_DIMENSION);
|
||||
const output = await image
|
||||
.rotate()
|
||||
.resize(targetSize, targetSize, {
|
||||
fit: 'cover',
|
||||
.resize({
|
||||
width: MAX_OUTPUT_DIMENSION,
|
||||
height: MAX_OUTPUT_DIMENSION,
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true
|
||||
})
|
||||
.webp({
|
||||
quality: WEBP_QUALITY,
|
||||
effort: 4
|
||||
})
|
||||
.toBuffer();
|
||||
.toBuffer({ resolveWithObject: true });
|
||||
|
||||
return {
|
||||
buffer: output,
|
||||
buffer: output.data,
|
||||
image: {
|
||||
format: 'webp',
|
||||
ext: 'webp',
|
||||
mime: 'image/webp',
|
||||
width: targetSize,
|
||||
height: targetSize,
|
||||
byteSize: output.length
|
||||
width: output.info.width,
|
||||
height: output.info.height,
|
||||
byteSize: output.data.length
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
const SITE_NAME = 'The Meme Protocol';
|
||||
const SITE_DESCRIPTION = 'A live, moderated meme stream for The Meme Protocol: square WebP memes, MEME_CONSENSUS_SCORE ranking, and community review.';
|
||||
const SITE_DESCRIPTION = 'A live, moderated meme stream for The Meme Protocol: WebP memes, MEME_CONSENSUS_SCORE ranking, and community review.';
|
||||
const REPO_URL = 'https://git.yoonect.com/Nautilus/bitsforfree';
|
||||
|
||||
export function publicBaseUrl(req) {
|
||||
@@ -76,7 +76,7 @@ export function llmsTxt(baseUrl) {
|
||||
return [
|
||||
'# The Meme Protocol',
|
||||
'',
|
||||
'> A live, moderated meme gallery. Uploads are square PNG/JPEG inputs normalized to metadata-stripped WebP, scored with MEME_CONSENSUS_SCORE, and published only after AI or admin approval.',
|
||||
'> A live, moderated meme gallery. Uploads are PNG/JPEG inputs normalized to metadata-stripped WebP, scored with MEME_CONSENSUS_SCORE, and published only after AI or admin approval.',
|
||||
'',
|
||||
'Important URLs:',
|
||||
`- Site: ${baseUrl}/`,
|
||||
|
||||
+13
-14
@@ -40,28 +40,27 @@ test('rejects gif uploads', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects non-square images when square uploads are required', () => {
|
||||
test('accepts non-square png uploads', () => {
|
||||
const png = encodePng(32, 24, () => [0, 255, 65, 255]);
|
||||
assert.throws(
|
||||
() => validateImage(png, {
|
||||
maxBytes: 5 * 1024 * 1024,
|
||||
maxWidth: 6000,
|
||||
maxHeight: 6000,
|
||||
maxPixels: 20_000_000,
|
||||
requireSquare: true
|
||||
}),
|
||||
/square/
|
||||
);
|
||||
const image = validateImage(png, {
|
||||
maxBytes: 5 * 1024 * 1024,
|
||||
maxWidth: 6000,
|
||||
maxHeight: 6000,
|
||||
maxPixels: 20_000_000
|
||||
});
|
||||
|
||||
assert.equal(image.width, 32);
|
||||
assert.equal(image.height, 24);
|
||||
});
|
||||
|
||||
test('normalizes png uploads to square webp', async () => {
|
||||
const png = encodePng(32, 32, () => [0, 255, 65, 255]);
|
||||
test('normalizes png uploads to webp while preserving aspect ratio', async () => {
|
||||
const png = encodePng(32, 24, () => [0, 255, 65, 255]);
|
||||
const normalized = await normalizeToWebp(png);
|
||||
|
||||
assert.equal(normalized.image.mime, 'image/webp');
|
||||
assert.equal(normalized.image.ext, 'webp');
|
||||
assert.equal(normalized.image.width, 32);
|
||||
assert.equal(normalized.image.height, 32);
|
||||
assert.equal(normalized.image.height, 24);
|
||||
assert.equal(normalized.buffer.subarray(0, 4).toString('ascii'), 'RIFF');
|
||||
assert.equal(normalized.buffer.subarray(8, 12).toString('ascii'), 'WEBP');
|
||||
});
|
||||
|
||||
+1
-2
@@ -39,8 +39,7 @@ test('increments view and download counters', async () => {
|
||||
maxBytes: 5 * 1024 * 1024,
|
||||
maxWidth: 6000,
|
||||
maxHeight: 6000,
|
||||
maxPixels: 20_000_000,
|
||||
requireSquare: true
|
||||
maxPixels: 20_000_000
|
||||
});
|
||||
const meme = await store.save({
|
||||
buffer,
|
||||
|
||||
Reference in New Issue
Block a user