Files
reactbin/specs/003-upload-thumbnails/spec.md
agatha f953c88984 Feat: Pre-generate WebP thumbnails on upload for faster library load
- Add Pillow dependency and thumbnail.py with generate_thumbnail() — produces
  WebP ≤400px, preserves aspect ratio, never upscales, handles GIF frame 0
- Alembic migration 002 adds nullable thumbnail_key column to images table
- Upload route generates thumbnail via asyncio.to_thread (non-blocking),
  stores at {hash}-thumb; failure is tolerated and upload succeeds with null key
- New GET /api/v1/images/{id}/thumbnail endpoint: serves WebP thumbnail or
  falls back to original for pre-feature images; ETag + immutable cache headers
- Delete route cleans up thumbnail storage object alongside original
- Library grid switches from /file to /thumbnail for all image src bindings
- 59 tests passing (46 existing + 13 new across unit, upload, serving, delete)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-03 17:26:16 +00:00

8.6 KiB
Raw Permalink Blame History

Feature Specification: Upload Thumbnails

Feature Branch: 003-upload-thumbnails Created: 2026-05-03 Status: Draft Input: User description: "When users load the UI, full size images are fetched, which may cause considerable load time when there are a lot of images -- a grid of 20 images could silently pull several hundred megabytes. We will solve this by pre-generating thumbnails on upload: when an image is uploaded, immediately produce one (or a few) fixed-size thumbnail variants and store them alongside the original. The library always fetches the thumbnail key, the detail page fetches the original key. Zero resize cost at serve time. A single fixed-size re-encoded as WebP for smaller bytes will cover the grid view."

User Scenarios & Testing (mandatory)

User Story 1 — Fast Library Load (Priority: P1)

A user opens the application or runs a tag-filtered search. The image grid loads quickly even when the library contains many images, because each grid cell fetches a compact thumbnail rather than the full-size original.

Why this priority: This is the core motivation. A grid of 20 images today could pull hundreds of megabytes; thumbnails bring that down to a few megabytes, making the library usable on slow or metered connections.

Independent Test: Upload 20 images of varying sizes (including some near the 50 MB limit). Open the library. Measure total bytes transferred while the grid loads. Compare against loading the same library before this feature. Verify the grid renders fully and that each thumbnail is visually recognisable as the correct image.

Acceptance Scenarios:

  1. Given a library with multiple images, When the user opens the library page, Then each grid cell displays a thumbnail that is visually recognisable as its image, and the total data transferred to render the full grid is substantially less than the sum of the original file sizes.

  2. Given a user applies one or more tag filters, When the filtered results are displayed, Then thumbnails are shown for all matching images with the same reduced data footprint.

  3. Given a library with images of mixed types (JPEG, PNG, GIF, WebP), When the grid loads, Then thumbnails for all types display correctly.


User Story 2 — Full-Size Image on Detail Page (Priority: P1)

A user clicks an image in the library grid to open its detail page. The full-size original is displayed, not the thumbnail. The experience is unchanged from before this feature.

Why this priority: The detail page is where the user inspects or copies an image; showing the thumbnail there would degrade the product's core value.

Independent Test: Open any image's detail page. Verify the image displayed matches the original resolution and file size, not the thumbnail dimensions.

Acceptance Scenarios:

  1. Given the user clicks an image thumbnail in the library, When the detail page loads, Then the full-size original image is displayed at its native resolution.

  2. Given the user navigates directly to an image's detail URL, When the page loads, Then the full-size original is displayed.


User Story 3 — Thumbnails Generated Automatically on Upload (Priority: P1)

A user uploads a new image. Without any additional action, a thumbnail is available immediately. There is no separate step or explicit request to generate a thumbnail.

Why this priority: The value of the feature depends entirely on thumbnails being present for every image. Manual generation or lazy generation would create inconsistencies in the grid.

Independent Test: Upload a new image. Immediately open the library. Verify the new image's thumbnail appears in the grid without any extra action.

Acceptance Scenarios:

  1. Given the user uploads a supported image, When the upload completes, Then a thumbnail is available and appears correctly in the library grid.

  2. Given the user uploads a duplicate image (already in the library), When the upload completes, Then no redundant thumbnail is generated — the existing thumbnail is reused.

  3. Given the user uploads an image at or near the maximum supported file size (50 MB), When the upload completes, Then the thumbnail is generated successfully and the upload response time remains acceptable.


Edge Cases

  • What happens when thumbnail generation fails during upload? → The upload still succeeds and the original image is stored; a fallback to the original is shown in the grid, or the item is hidden until the thumbnail is available (assumption: fall back to original rather than silently drop the image).
  • What happens when an image is deleted? → Both the original and its thumbnail are removed from storage.
  • What happens with existing images that were uploaded before this feature? → Those images have no pre-generated thumbnail; the grid falls back to the original for those entries until a backfill is performed (backfill is out of scope for v1 of this feature).
  • What happens with animated GIFs? → A static thumbnail is generated from the first frame.

Requirements (mandatory)

Functional Requirements

  • FR-001: The system MUST generate a thumbnail for every newly uploaded image as part of the upload operation, before the upload response is returned to the caller.
  • FR-002: Thumbnails MUST be stored in the same object storage as the original, addressable by a distinct key derived from the image.
  • FR-003: The thumbnail MUST be encoded as WebP regardless of the original image format.
  • FR-004: The thumbnail MUST fit within a fixed maximum dimension on its longest side, preserving the original aspect ratio; no dimension of the thumbnail MAY exceed 400 pixels.
  • FR-005: The library grid view MUST fetch and display thumbnails instead of original images.
  • FR-006: The image detail view MUST continue to fetch and display the full-size original.
  • FR-007: When a duplicate image is uploaded, the thumbnail MUST NOT be regenerated or re-stored; the existing thumbnail is reused.
  • FR-008: When an image is deleted, its thumbnail MUST also be deleted from storage.
  • FR-009: If thumbnail generation fails during upload, the upload MUST still succeed; the system MUST fall back to serving the original image in the grid for that entry.
  • FR-010: The API MUST expose a way for clients to retrieve the thumbnail content for a given image, distinct from the full-size content endpoint.

Key Entities (include if feature involves data)

  • Image: Gains a new optional attribute indicating whether a thumbnail is available and the key under which the thumbnail is stored.
  • Thumbnail: A derived, smaller representation of an Image. Key attributes: storage key, dimensions (width × height), format (WebP), relationship to its source Image.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: The total data transferred to render a 20-image library grid is reduced by at least 80% compared to fetching full-size originals for the same images.
  • SC-002: The library grid's first page loads in under 2 seconds on a local network connection for a library of 1,000 images, with thumbnails visible without a second load.
  • SC-003: Thumbnails are available immediately after upload completes — no polling or manual refresh is required.
  • SC-004: The detail page continues to show the full-size original; no regression in detail-page image quality is introduced.
  • SC-005: Deleting an image removes both the original and its thumbnail; no orphaned thumbnail objects remain in storage after deletion.

Assumptions

  • A single thumbnail size (longest side ≤ 400 px, WebP) is sufficient for the library grid view in v1. Additional sizes or formats are out of scope.
  • Thumbnail generation happens synchronously during the upload request. Async background processing is not required for v1.
  • Existing images uploaded before this feature are not automatically backfilled with thumbnails in v1; the grid falls back to the original for those entries.
  • Animated GIF thumbnails capture only the first frame; animation is not preserved in the thumbnail.
  • The thumbnail storage key is derived deterministically from the image's existing content hash, so no additional database column is strictly required to locate it — however the Image record will track thumbnail availability for correctness.
  • No change is required to tag management, duplicate detection, or any other upload behaviour beyond adding thumbnail generation.