Files
pi-weekly-newspaper/docs/superpowers/specs/2026-04-06-mosaic-cover-design.md
cottongin 8deb71b81a docs: add mosaic cover design spec
Made-with: Cursor
2026-04-06 19:16:12 -04:00

63 lines
4.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (`#f5f5f5` to 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_cover` in `src/cover.py` currently receives `headlines` and `categories`. It needs access to the article images.
- We will modify the call signature of `generate_cover` to accept a list of image paths (or the `Article` objects themselves). Passing a list of local image paths `image_paths: list[str]` is the cleanest approach.
- In `src/routes/publish.py` and `src/scheduler.py`, when preparing the issue, we will query the `images` table to find the first image for each included article and pass these paths to `generate_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 `RGBA` image 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 (using `ImageOps.contain` or similar aspect-ratio-preserving resize), and paste it centered on the canvas.
- **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"` to `generate_mosaic_cover`. Remove the `generate_ai_cover` function and Pollinations.ai dependency.
- **Config (`config.py`):** Remove `POLLINATIONS_URL`.
## Files Modified
- `src/cover.py`: Add `generate_mosaic_cover`, remove `generate_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.