4.2 KiB
4.2 KiB
Mosaic Cover Design
Date: 2026-04-06 Scope: Replace the AI-generated cover background with a mosaic of article images.
Context
The weekly newspaper currently uses Pollinations.ai to generate a background image for the cover based on the dominant category of the week's articles. We want to replace this with a deterministic mosaic of the actual images featured in the week's articles.
Requirements
- Source: Use the first image (if any) from each article included in the issue.
- Filtering: Articles without images are skipped.
- Layout: Column Masonry (2 columns).
- Sizing: Images are scaled to fit the column width (234px) while preserving their natural aspect ratio. No cropping or letterboxing within the cells.
- Spacing: 4px gap between images and around the edges.
- Canvas: 480×800 pixels with a white background (
#f5f5f5to match the programmatic text cover). - Single Article: If the issue has only one article (and it has an image), center that single image on the canvas instead of building a mosaic.
- Fallback: If no articles have images, fall back to the existing text-only cover.
- Overlays: The existing masthead, date, and headline overlays (
_draw_text_overlays) remain unchanged and are drawn over the mosaic.
Architecture & Implementation
1. Image Extraction
generate_coverinsrc/cover.pycurrently receivesheadlinesandcategories. It needs access to the article images.- We will modify the call signature of
generate_coverto accept a list of image paths (or theArticleobjects themselves). Passing a list of local image pathsimage_paths: list[str]is the cleanest approach. - In
src/routes/publish.pyandsrc/scheduler.py, when preparing the issue, we will query theimagestable to find the first image for each included article and pass these paths togenerate_cover.
2. Mosaic Algorithm (src/cover.py)
- Create a new function
generate_mosaic_cover(output_dir, week_start, week_end, headlines, image_paths). - Canvas Setup: Create a 480×800
RGBAimage with a(245, 245, 245, 255)background. - Single Image Case:
- If
len(image_paths) == 1: Open the image, resize it to fit within 480×800 (usingImageOps.containor similar aspect-ratio-preserving resize), and paste it centered on the canvas.
- If
- Mosaic Case:
- Define columns: 2 columns.
- Define gap: 4px.
- Column width:
(480 - (3 * 4)) // 2 = 234px. - Track the current Y position (height) of each column:
col_heights = [4, 4]. - Iterate through
image_paths:- Open the image.
- Calculate new height:
new_h = int(img.height * (234 / img.width)). - Resize image to
(234, new_h). - Find the shortest column:
col_idx = 0 if col_heights[0] <= col_heights[1] else 1. - Check if adding this image exceeds the canvas height (800px). If
col_heights[col_idx] + new_h > 800, we can either stop adding images or crop the bottom of the image. Since we want to fill the canvas but not exceed it, we will paste the image and let it be clipped by the canvas bounds. We will stop processing images once both columns exceed 800px. - Paste the image at
x = 4 + col_idx * (234 + 4),y = col_heights[col_idx]. - Update column height:
col_heights[col_idx] += new_h + 4.
- Overlays: Call
_draw_text_overlays(draw, 480, 800, week_start, week_end, headlines). - Save as JPEG.
3. UI and Integration Updates
- UI (
templates/publish.html&templates/settings.html): Change "AI Cover" option to "Mosaic Cover" (value"mosaic"). - Backend (
src/routes/publish.py,src/scheduler.py,src/cover.py): Update logic to route"mosaic"togenerate_mosaic_cover. Remove thegenerate_ai_coverfunction and Pollinations.ai dependency. - Config (
config.py): RemovePOLLINATIONS_URL.
Files Modified
src/cover.py: Addgenerate_mosaic_cover, removegenerate_ai_cover.src/routes/publish.py: Extract image paths, update cover method string.src/scheduler.py: Extract image paths, update cover method string.templates/publish.html: Update UI labels.templates/settings.html: Update UI labels.config.py: Remove unused AI URL.tests/test_cover.py: Update tests for mosaic generation.