Skip to content

feat: responsive image delivery with multi-size PNG + WebP#5192

Merged
MarkusNeusinger merged 4 commits intomainfrom
claude/implement-issue-5191-Z6eyk
Mar 30, 2026
Merged

feat: responsive image delivery with multi-size PNG + WebP#5192
MarkusNeusinger merged 4 commits intomainfrom
claude/implement-issue-5191-Z6eyk

Conversation

@MarkusNeusinger
Copy link
Copy Markdown
Owner

Summary

  • Replace single plot_thumb.png with responsive multi-size PNG + WebP variants (400/800/1200px) for all plot images
  • Frontend <picture> elements with srcSet + sizes let the browser pick optimal size based on viewport and DPR
  • Remove preview_thumb field from API, database sync, metadata templates, and all documentation
  • Graceful fallback to plot.png during migration (before backfill runs)

Image savings (scatter-basic/matplotlib example)

Variant Size vs original
plot.png (original) 477K baseline
plot_800.webp (typical mobile) 18K -96%
plot_400.webp (small mobile) 7.2K -98%

Changes (33 files)

  • Backend: create_responsive_variants() in core/images.py generates all 7 variants
  • Frontend: <picture> + srcSet in ImageCard, SpecDetailView, SpecOverview, CatalogPage
  • Workflows: impl-generate.yml and impl-repair.yml generate + upload all variants
  • API: Removed preview_thumb/thumb from schemas, routers, MCP server
  • Scripts: backfill_responsive_images.py for historical plots, remove_preview_thumb_from_yaml.py for YAML cleanup

Post-merge steps

  1. Run python scripts/backfill_responsive_images.py to generate variants for ~2,668 existing images (~2h)
  2. Run python scripts/remove_preview_thumb_from_yaml.py → commit YAML cleanup separately
  3. Later: Alembic migration to drop preview_thumb DB column

Closes #5191

Test plan

  • 1061 unit tests pass
  • Frontend builds successfully
  • Ruff lint + format clean
  • Local test: responsive variants generated with correct sizes
  • Deploy and verify fallback to plot.png for historical images
  • Run backfill script on production GCS
  • Trigger impl-generate for one spec to verify new workflow

🤖 Generated with Claude Code

claude and others added 3 commits March 30, 2026 18:42
Backend: add create_responsive_variants() to generate 400/800/1200px
PNGs and WebPs plus full-size WebP from source plot.png.

Frontend: replace <img> with <picture> + srcSet/sizes in ImageCard,
SpecDetailView, and SpecOverview. Browser auto-selects optimal size
based on viewport and DPR. Falls back to existing thumb/url if
responsive variants are not yet available.

https://claude.ai/code/session_01WUQ7f2vcScXKtqLKgfdRtw
…iants

- Removed preview_thumb references from metadata and code
- Added functionality to generate responsive image variants (PNG and WebP)
- Updated upload process to handle multiple image formats
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 30, 2026 19:48
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 30, 2026

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements responsive plot image delivery by generating and serving multi-size PNG/WebP variants (400/800/1200) derived from preview_url, while removing the legacy preview_thumb field across backend, frontend, workflows, tests, and documentation.

Changes:

  • Backend adds responsive variant generation and updates scripts/workflows to produce and upload all variants.
  • Frontend switches plot rendering to <picture> with srcSet/sizes and migration fallback to plot.png.
  • API/contracts/docs/tests are updated to remove preview_thumb / thumb.

Reviewed changes

Copilot reviewed 33 out of 33 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/unit/automation/scripts/test_sync_to_postgres.py Updates expected synced implementation payloads to remove preview_thumb.
tests/unit/api/test_routers.py Removes thumb-related fixtures/attributes from router unit tests.
tests/unit/api/test_main.py Removes thumb-related fixtures/attributes from app startup/unit tests.
tests/unit/api/mcp/test_tools.py Removes preview_thumb usage in MCP tool tests.
tests/e2e/conftest.py Removes thumb URL constant and preview_thumb from seeded e2e data.
tests/conftest.py Removes thumb URL constant and preview_thumb from seeded test data.
scripts/remove_preview_thumb_from_yaml.py Adds one-time script to strip preview_thumb from metadata YAML files.
scripts/backfill_responsive_images.py Adds backfill script to generate/upload responsive variants for historical GCS images.
prompts/templates/metadata.yaml Updates metadata template to only include preview_url (variants derived by convention).
prompts/templates/library-metadata.yaml Updates per-library metadata template to remove preview_thumb URL.
docs/reference/repository.md Updates repository reference docs to remove preview_thumb and legacy thumbnail naming.
docs/reference/database.md Updates DB reference docs to remove preview_thumb from examples/schema description.
docs/reference/api.md Updates API reference docs to remove preview_thumb/thumb from payload examples.
core/images.py Adds create_responsive_variants() and CLI command for generating variants.
core/database/repositories.py Removes preview_thumb from repository field sets used for DB updates.
automation/scripts/sync_to_postgres.py Stops reading/updating preview_thumb from metadata during DB sync.
app/src/utils/responsiveImage.ts Adds utilities to derive variant URLs and responsive sizes/srcSet from plot.png.
app/src/types/index.ts Removes thumb/preview_thumb from frontend types.
app/src/pages/CatalogPage.tsx Switches catalog hero/preview image rendering to <picture> with responsive variants + fallback.
app/src/components/SpecOverview.tsx Updates implementation cards to render responsive images via <picture>.
app/src/components/SpecDetailView.tsx Updates detail view image to responsive <picture> with fallback handling.
app/src/components/ImageCard.tsx Replaces CardMedia with <picture> and uses responsive utilities + fallback logic.
app/src/components/ImageCard.test.tsx Updates tests to assert new fallback src behavior (plot_800.png).
api/schemas.py Removes preview_thumb/thumb fields from Pydantic response models.
api/routers/specs.py Removes preview_thumb from spec response and spec-images payload.
api/routers/plots.py Removes thumb from collected image dicts for filtered plot responses.
api/routers/og_images.py Updates OG collage generation to fetch preview_url (no longer prefers thumbnail).
api/routers/libraries.py Removes thumb from library images endpoint payload.
api/mcp/server.py Removes preview_thumb from MCP spec detail responses (but not fully from file).
agentic/docs/project-guide.md Updates project guide metadata example to remove preview_thumb.
agentic/commands/update.md Updates update command docs to remove thumbnail generation/upload steps.
.github/workflows/impl-repair.yml Generates responsive variants in repair workflow and uploads all plot image variants.
.github/workflows/impl-generate.yml Generates responsive variants in generate workflow and uploads all plot image variants.

Comment thread core/images.py
Comment on lines +244 to +257
for width in target_sizes:
# Skip sizes larger than the original
if width >= img.width:
resized = img
actual_width, actual_height = img.width, img.height
else:
ratio = width / img.width
actual_width = width
actual_height = int(img.height * ratio)
resized = img.resize((actual_width, actual_height), Image.Resampling.LANCZOS)

for ext, fmt, opts in RESPONSIVE_FORMATS:
out_path = output_dir / f"plot_{width}.{ext}"
resized.save(out_path, fmt, optimize=True, **opts)
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In create_responsive_variants(), when the source image is narrower than a target width, the code reuses the original image but still writes it to a filename like plot_1200.png and the frontend will advertise it as 1200w in srcset. That breaks srcset semantics (declared width != actual) and can cause the browser to pick an overly-large candidate and render a blurry/upscaled image. Consider either (a) always resizing/upscaling to the requested width so the file and descriptor match, or (b) explicitly enforce a minimum source width (raise/log+skip generation) so plot.png is guaranteed to be >= max responsive size.

Copilot uses AI. Check for mistakes.
Comment thread core/images.py Outdated
Comment on lines +236 to +238
img = Image.open(input_path)
if img.mode in ("RGBA", "P"):
img = img.convert("RGB")
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

create_responsive_variants() opens the source image via Image.open() without closing it. This can leak file descriptors and also interfere with deleting the input file on some platforms (notably Windows) in batch/backfill scripts. Prefer using a context manager (with Image.open(...) as img:) and copying the image object before converting/resizing as needed.

Suggested change
img = Image.open(input_path)
if img.mode in ("RGBA", "P"):
img = img.convert("RGB")
with Image.open(input_path) as src:
if src.mode in ("RGBA", "P"):
img = src.convert("RGB")
else:
img = src.copy()

Copilot uses AI. Check for mistakes.
Comment thread core/images.py
Comment on lines +213 to +216
def create_responsive_variants(
input_path: str | Path, output_dir: str | Path, sizes: list[int] | None = None, optimize: bool = True
) -> list[dict[str, str | int]]:
"""Generate multi-size, multi-format image variants for responsive delivery.
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New responsive variant generation logic (create_responsive_variants) isn’t covered by unit tests, while this module already has tests for create_thumbnail/process_plot_image (tests/unit/core/test_images.py). Adding tests that assert expected files are created, dimensions match requested widths, and WebP/PNG variants are generated (including behavior when the source is smaller than 1200px) would help prevent regressions.

Copilot uses AI. Check for mistakes.
Comment thread api/routers/og_images.py
Comment on lines 168 to 171
try:
# Fetch all images in parallel — prefer thumbnails (smaller, faster)
images = list(
await asyncio.gather(*[_fetch_image(impl.preview_thumb or impl.preview_url) for impl in selected_impls])
)
# Fetch all images in parallel
images = list(await asyncio.gather(*[_fetch_image(impl.preview_url) for impl in selected_impls]))
labels = [f"{spec_id} · {impl.library_id}" for impl in selected_impls]
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_spec_collage_image now fetches impl.preview_url (full plot.png) for up to 6 images. That can significantly increase bytes transferred and image processing time for OG image generation. Since responsive variants are now available by convention, consider fetching a smaller variant (e.g., plot_400.png/plot_800.png) and falling back to plot.png if the variant 404s during the migration window.

Copilot uses AI. Check for mistakes.
Comment thread api/mcp/server.py
Comment on lines 273 to 279
impl_response = ImplementationResponse(
library_id=impl.library.id,
library_name=impl.library.name,
preview_url=impl.preview_url,
preview_thumb=impl.preview_thumb,
preview_html=impl.preview_html,
quality_score=impl.quality_score,
code=impl.code,
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

preview_thumb was removed from API schemas, but this file still passes preview_thumb in get_implementation (later in the file). With Pydantic v2 this will be treated as an extra field and ignored, which can mask drift between the MCP server output and the declared schema. Remove the remaining preview_thumb usage so MCP responses consistently match ImplementationResponse.

Copilot uses AI. Check for mistakes.
- Fix missed preview_thumb in MCP server get_implementation
- Use context manager for Image.open() to prevent fd leaks
- Skip responsive variants larger than source (correct srcset semantics)
- OG images: try plot_800.png first for efficiency, fallback to plot.png
- Add 7 unit tests for create_responsive_variants()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@MarkusNeusinger MarkusNeusinger merged commit 3d0aef3 into main Mar 30, 2026
7 checks passed
@MarkusNeusinger MarkusNeusinger deleted the claude/implement-issue-5191-Z6eyk branch March 30, 2026 20:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Responsive Image Delivery: Multi-Size PNG + WebP (400/800/1200)

3 participants