PlantTracer Movie Processing Phases

This note sketches the staged refactor of movie upload and processing.

Phase 1 – Infra / Bootstrap / Events

  • Remove the S3 → Lambda event architecture for the uploads/ prefix:

    • Delete the LambdaS3InvokePermission resource in template.yaml so S3 no longer invokes the Lambda directly.

    • Stop configuring S3 bucket notifications in etc/bootstrap.sh (drop the call to etc/s3_upload_trigger.py).

    • Remove the bootstrap S3-based ping that uploads uploads/_bootstrap/ping.json and instead rely on the Lambda HTTP /status endpoint for health checks.

  • Lambda remains reachable only via its HTTP API (AWS::Serverless::HttpApi); no behavior change for existing HTTP routes in this phase.

  • Update docs and rules that describe the S3 → Lambda trigger so they reflect the HTTP-only model.

Phase 2 – Flask / JavaScript Orchestration

  • Change the upload pipeline to:

    • Have the browser call a status API to confirm the Lambda is healthy before starting an upload.

    • Use the upload API to create the movie record and mark its status as uploading.

    • Upload movies directly to their final S3 keys via presigned POST (no uploads/ staging prefix).

    • After a successful upload, have the browser call a new Lambda processing API to kick off processing.

  • On the server side (Flask):

    • Simplify /api/new-movie to always presign the final key instead of choosing between staging and final based on environment.

    • Add or expose movie-level processing state in the movies table (e.g., processing_state, first_frame_urn, movie_zipfile_urn).

    • Move movie rotation from VM ffmpeg into a Lambda-driven API that queues rotation/processing commands.

  • In the browser:

    • Wire the upload page to use the new status and start-processing APIs.

    • After upload, initiate processing and poll for the first frame and zipfile readiness using the new state fields.

Phase 3 – UI Refinements and Status Display

  • Make movie processing state visible and understandable in the UI:

    • Extend the movie list to show upload / processing / rotation status for each movie (e.g., uploading, processing, first-frame-ready, zip-ready, rotating). Done: list shows processing_state in the Status column.

    • Improve messaging on the analyze page when processing is still running (poll for up to 60 seconds, then ask the user to return later). Done: #status-big shows “Waiting for processing to complete…” while polling; on timeout “Processing did not complete in time. Please come back later.”

    • Clarify first-frame behavior after upload (250ms polling with a clear error if the Lambda is not responding). Done: 250ms × 10 attempts; clear error message and no broken image icon.

  • Note (in comments/docs and code): the movie list page should eventually be re-architected around server-side rendering with Jinja2; for now it continues to rely on the existing JSON APIs and client-side rendering. Done: comment in flask_app.py at /list and in planttracer.js at list_movies_data.

Rotation and zip behavior (Phase 2)

To avoid queuing multiple rotation requests when the user clicks “Rotate” several times, the client debounces rotate clicks: the user may click 1, 2, or 3 times (for 90°, 180°, or 270°); after about one second with no further click, the client sends a single request to /api/edit-movie with action=rotate90cw and rotation_steps=1|2|3.

VM (Option A): The VM does not rotate the movie or build the zip. It only: (1) clears all tracking for the movie (trackpoints and last_frame_tracked), (2) updates the movie’s rotation_steps in the DB, and (3) triggers the Lambda via HTTP POST to the rotate-and-zip API. The request returns immediately; Lambda performs rotation, zip build, and metadata write (width/height/fps/total_frames/total_bytes, with width/height swapped for 90°/270°) asynchronously.

The user can go to the Analyze page immediately. The first frame shown comes from get-frame (which may still be the pre-rotate movie until Lambda finishes). When the zip is ready, the analyze page polls get-movie-metadata every 2 seconds. When movie_zipfile_url appears, the page loads the zip and switches to the full frame set. If the zip does not appear within 60 seconds, the page shows “Processing did not complete in time. Please come back later.”

Rotation in Lambda (no ffmpeg)

Rotation and zip creation run only in the Lambda. The VM does not rotate or build zip. The Lambda does not use the ffmpeg binary (which would add ~150MB to the deployment). It uses PyAV (av) and Pillow only: decode video with av, rotate each frame with Pillow, re-encode with av (mpeg4), build the frame zip, and write full movie metadata (width, height, fps, total_frames, total_bytes) to DynamoDB. All dependencies are in the Lambda group (poetry group lambda).

Lambda-only code and testing

The Lambda can depend on code and packages that the main Flask API does not (e.g. resize_app/rotate_zip.py, PyAV). The single pyproject.toml at repo root defines group lambda for Lambda-only deps; make install-lambda-deps (or poetry install --with lambda) installs everything needed to build and run the Lambda. For testing: run the Lambda handler locally with mock HTTP events, or add a second Lambda later and ensure CI runs install --with lambda and any Lambda-specific tests. See lambda-resize/Makefile (vend-app, install) and the root Makefile targets for lambda-resize.