Cloudinary API Integration
SECTION 26 — CLOUDINARY & IMAGE LIFECYCLE MANAGEMENT
26.1 Three-Tier Image Storage Architecture
26.2 Image Upload Flow
- Teacher uploads an image (drag-and-drop or file picker on thumbnail slot), OR pastes web content containing inline
<img>tags into a Brief field (§10.9). - Image is sent to the FastAPI backend.
- Backend uploads image to Cloudinary with WebP as the storage format (
f_webpupload transformation, lossless or quality-auto per §26.6) and receives the CDN URL. - CDN URL is stored in Neon DB (in the document’s JSON schema
images.thumbnailor appended to the Brief Lexical tree as an image node — see §10.9; the derivedimages.reference-imagesarray is rebuilt by the backend on save). - The document JSON never stores raw image files — only Cloudinary CDN URLs.
- Metadata record for the image is created in the Neon DB
imagestable:\{ image-id, cloudinary_url, cloudinary_public_id, document_id, news_item_headline, category, document_date, teacher_id, editor_id, upload_timestamp, academic_year, image_type, atomic_uid, crop, origin \}. - Crop transforms (when applied via §10.8) are recorded as
crop = \{ x, y, w, h, applied_at \}on the image record and mirrored to the document JSON. The original WebP asset is never overwritten by a crop operation — render-time URL params apply the crop instead.
26.3 Image Naming Convention (Drive)
When images are synced to Google Drive, they are renamed following this convention:n= order index of the image within the document (1, 2, 3…)HEADLINE[:10]= first 10 characters of the news headline (stripped of special characters)CATEGORY= news category nameDD-MM-YY= document dateextension= original file extension (jpg, png, webp, etc.)
26.4 Academic Year Boundary & Year-End Archival
v5 note: When a Teacher enables the Full-AY Archive toggle (§26A), the image-only archival flow described below is subsumed by the full-AY ZIP. The cron job still fires at the same boundary, but it builds a single per-AY archive containing JSON + PDF + PPTX + images instead of an images-only ZIP. When the toggle is off (default), the image-only flow below applies as written.
- Default academic year: January 1 — December 31 (calendar year)
- Per-Teacher override: Each Teacher can configure a custom academic year start month in Account Settings. The archival cron job evaluates each Teacher’s boundary independently.
- Data retention grace period: Cloudinary data is retained for one full calendar month after the academic year ends. Deletion is scheduled at the end of the month following the academic year’s final month.
- Example: Academic Year 2026 (January 2026 – December 2026) → archival and deletion is scheduled on January 31, 2027.
- Example: Custom academic year April 2026 – March 2027 → archival and deletion is scheduled on April 30, 2027.
- Retrospective document creation: During the one-month grace period, Teachers can still create documents retrospectively for dates in the previous academic year. Backend checks
cloudinary_data_retention_active = truefor the previous year before allowing retrospective document creation. Once the grace period expires and deletion fires, retrospective document creation for the previous academic year is permanently disabled.
- Cron job fires at the end of the month following the Teacher’s configured academic year end date (one-month grace period).
- Backend queries all Cloudinary images for the concluding academic year for that Teacher.
- Images are downloaded from Cloudinary.
- Python script compresses images (without quality loss) and packages them into a ZIP archive.
- ZIP archive is uploaded to the Teacher’s Google Drive at:
YYYY/ARCHIVE/IMAGES-YYYY.zip. - Once Drive upload is confirmed, backend calls Cloudinary API to delete all archived images.
- Neon DB image records are updated:
\{ archived: true, cloudinary-url: null, drive-archive-path: "YYYY/ARCHIVE/IMAGES-YYYY.zip", cloudinary_data_retention_active: false \}. - Backend sets
retrospective_creation_allowed = falsefor the concluded academic year — Teachers can no longer create documents for dates in that year.
26.5 PDF Compression
- Python backend applies PDF compression to all PDFs stored on the backend before persistence and (optional) Drive sync.
- Compression must not result in visible quality loss.
- Developer should evaluate libraries such as
ghostscript(via Python subprocess) orpikepdffor this purpose. - Scope:
- WeasyPrint-generated branded PDFs (§17.6) — compressed on the server immediately after WeasyPrint completes. Always.
- Reveal.js slide-deck PDFs (§17.2) — compressed only for the backend-stored copy. The backend copy is produced by either (a) the auto-persistence step triggered at
event_type = "delivered"(server-side headless-browser print), or (b) any future flow that uploads a slide-deck PDF to the backend. Compression runs as a preliminary step immediately after the PDF file is saved to the backend store. - Local device save of the Reveal.js print-to-PDF (Teacher invokes the browser print dialog and saves to disk) is NOT compressed — it is the raw browser output, lossless and highest-fidelity. No preliminary step runs on the local path.
- Rationale: compression is a backend-cost optimisation. The user’s local artefact is unaffected — they keep the highest-quality copy possible, while the backend stores the lean variant.
26.6 Image Storage Codec (WebP) & Render Transcoding
- Storage codec: All images uploaded to Cloudinary are stored as WebP (
f_webpon upload). WebP delivers ~25–35% smaller file sizes vs JPEG at perceptually equivalent quality. - Quality setting:
q_auto:goodfor photographic content;q_auto:bestfor charts/infographics (auto-detected by Cloudinary). - Browser display: Browsers consume WebP directly (modern browser support is universal in our minimum-supported set — see §31.7). No transcoding needed for canvas display.
- Render-time transcoding for PDF/PPTX:
- WeasyPrint (PDF generation, §17.6): Backend fetches the Cloudinary asset with
f_jpgtransformation to receive a JPEG variant inline. WeasyPrint embeds the JPEG. The original WebP master stays untouched. - PptxGenJS (client-side PPTX generation, §17.1): Browser fetches the Cloudinary asset with
f_jpg(orf_pngif the source has alpha). PptxGenJS embeds JPEG/PNG natively — it does not consume WebP. - HTML export (§17.7): Bundled image assets are converted to JPEG (or PNG if alpha) during export to maximise compatibility for offline viewing. CDN URLs are rewritten to local relative paths.
- WeasyPrint (PDF generation, §17.6): Backend fetches the Cloudinary asset with