A serverless API for road pavement analysis using machine learning inference. The system processes road images and videos to detect pavement conditions, cracks, signals, and other infrastructure features.
This project implements a serverless architecture on AWS for processing road pavement data. It uses AWS Lambda, API Gateway, Step Functions, SageMaker, and other services to provide a scalable API for submitting analysis tasks and retrieving results.
The system consists of the following main components:
- API Gateway: REST API endpoint with Cognito authentication
- Task Microservice (Lambda): Main API handler for task management
- Inference Queue (SQS): Receives a message for every submitted task
- EventBridge Pipe: Routes queue messages to the inference response workflow (no glue code)
- Inference Response Workflow (Step Functions): Consumes the message and writes a terminal status back to the task. This is currently a placeholder (see Task Processing Flow) — it returns a simulated result without running SageMaker, and will be replaced by the SageMaker workflow below once that pipeline is wired to the queue.
- Road Section Inference Workflow (Step Functions): The real inference orchestration (not yet wired to the queue)
- Build Payload (Lambda): Prepares input data for SageMaker
- SageMaker Processing Job: Runs ML inference in containers (CPU/GPU)
- Discover Outputs (Lambda): Catalogs output files
- DynamoDB: Task metadata and status
- S3: Input files (attachments), output results (datalake), artifacts (models)
- Glue Catalog: Metadata for parquet files in datalake
- ECR: Docker images for CPU and GPU inference containers
- Main Handler (
app.py): APIGatewayRestResolver with router for/tasks - Controller (
controllers/task_controller.py): REST endpointsGET /tasks: List tasksPOST /tasks: Create taskGET /tasks/{taskId}: Get task detailsPUT /tasks/{taskId}: Update taskPOST /tasks/{taskId}/generateAttachmentUploadUrl: Generate S3 upload URLPOST /tasks/{taskId}/submit: Submit task for processingGET /taskTypes: List available task types
- Services:
TaskService: CRUD operations on tasksQueueService: Send messages to SQSStorageService: Generate presigned S3 URLs
- Models: Task, S3Object data structures
- Uses
InferenceBuilderto prepare SageMaker job parameters from task inputs
- Lists S3 output files and formats for DynamoDB storage
- Entrypoint (
entrypoint.py): Main processing script - Container Code (
sagemaker_container.py): Argument parsing and utilities - Supports input types:
video_gps: Video file with GPS dataimage_bundle_gps: Image bundles with GPSimage_bundle: Image bundles only
- Uses
pavimentadoslibrary for ML processing - Outputs: sections, signals_detected, failures_detected, detections_over_photogram
- CPU version:
src/ecr/road-section-inference-cpu/Dockerfile - GPU version:
src/ecr/road-section-inference-gpu/Dockerfile
The real ML inference orchestration. Not currently triggered — it will replace the placeholder workflow below once wired to the inference queue. States:
- BuildPayload: Prepare SageMaker parameters
- SetProcessingStatus: Update task status
- InferenceRoadSection: Run SageMaker processing job
- DiscoverOutputs: Catalog output files
- SetCompletedStatus: Mark task complete Error handling: SetFailedStatus on failures
Placeholder. This workflow stands in for the SageMaker pipeline so the end-to-end task lifecycle works today. It does not run any inference — it simply writes a terminal status back to the task, mirroring the real workflow's DynamoDB transitions. When the SageMaker pipeline is ready, the EventBridge Pipe target is repointed at
RoadSectionInferenceWorkflowand this workflow (plus theForceStatustesting hook) is removed.
It is invoked by the EventBridge Pipe for each message on the inference queue. States:
- ParseInput: Parse the task payload from the SQS message body
- SetProcessingStatus: Update task status to
processing - ProcessingDelay: Brief wait so the status transition is observable
- EvaluateResult (Choice):
failedifInputs.ForceStatus == "failed", otherwisecompleted - SetCompletedStatus / SetFailedStatus: Write the terminal status and message
- Glue Database:
pavimentados{env}(e.g.,pavimentados_dev) - Tables:
sections: Road section analysis resultssignals_detected: Detected traffic signalsfailures_detected: Pavement failuresdetections_over_photogram: Detailed detections
- Partitioned by: user, geography, task
The project uses AWS SAM for deployment.
- AWS CLI configured
- SAM CLI installed
- Docker (for building containers)
-
Build the application:
sam build
-
Deploy to development:
sam deploy --config-env default
-
Deploy to production:
sam deploy --config-env prod
The deployment requires several parameters defined in samconfig.toml:
UserPoolId: Cognito user pool IDInfraTableName: DynamoDB table nameAttachmentsBucketName: S3 bucket for inputsDatalakeBucketName: S3 bucket for outputsArtifactsBucketName: S3 bucket for modelsProcessorType:cpuorgpuMode:dev,test, orprod
Run unit tests with pytest:
pytest tests/unit/Run functional tests:
pytest tests/functional/Test the API locally using SAM Local:
-
Start local API:
sam local start-api -
Test endpoints (requires authentication setup)
After deployment, test against the deployed API URL (output from sam deploy).
A ready-to-use collection lives at
postman/Pavimentados-Tasks.postman_collection.json.
It exercises the full /tasks lifecycle plus /taskTypes.
- Import: in Postman, Import → select the JSON file.
- Set collection variables (collection → Variables tab):
baseUrl: your deploy URL, e.g.https://xxxx.execute-api.us-east-1.amazonaws.com/dev(theApiUrlstack output).accessToken: a Cognito IdToken (noBearerprefix). The collection sends it as a bearer token.taskId: leave empty — Create task captures it automatically into this variable.
- Run in order: List tasks → Create task → Get task → Update task → Generate attachment upload URL → Submit task → List tasks again. After Submit task, re-run Get task by id a few seconds later to watch the status move to
completed. - To demo the failed path: edit the Create task body and add
"ForceStatus": "failed"insideInputs, then run Create → Submit → Get; the task ends asfailed.
The API uses Cognito User Pool authentication. Include the Authorization header with a valid JWT token.
-
Create Task:
Inputs.Typeselects the input shape — one ofimage_bundle,image_bundle_gps, orvideo_gps. All shapes requireGeographyandGeographySource.POST /tasks { "Name": "Road Analysis Task", "Description": "Analysis of highway section", "Inputs": { "Type": "image_bundle", "Geography": "Pichincha", "GeographySource": "manual" } }
-
Upload Files:
FieldNameis the attachment field of the chosen input type (ImageBundle,VideoFile, orGpsFile).ArrayLengthapplies to array fields likeImageBundle.POST /tasks/{taskId}/generateAttachmentUploadUrl { "FieldName": "ImageBundle", "Extension": "zip", "ArrayLength": 1 }
Use the returned presigned URL(s) to upload the file(s).
-
Submit Task (only allowed from
draft):POST /tasks/{taskId}/submit
This sets the task to
queuedand enqueues a message on the inference queue. -
Check Status (poll until
completedorfailed):GET /tasks/{taskId}
-
List Tasks:
GET /tasks
POST /submit ──> set status=queued ──> SQS (inference queue)
│
EventBridge Pipe (batch size 1)
│
▼
Inference Response Workflow (Step Functions, placeholder)
│
set status=processing ─> (delay) ─> evaluate
│
┌────────────────┴────────────────┐
▼ ▼
status=completed status=failed
Placeholder behaviour: the inference response workflow does not run SageMaker. By default a submitted task ends as
completed. To exercise the failed path, set"ForceStatus": "failed"insideInputswhen creating the task.ForceStatusis a temporary testing hook and will be removed when the real SageMaker workflow is wired in.
On submit, the task microservice sends this JSON body to the inference queue
(build_event_payload in models/base_task.py):
{
"Id": "<task uuid>",
"Name": "Road Analysis Task",
"AccessLevel": "app",
"AppServiceSlug": "pavimenta2#road_sections_inference",
"UserSub": "<cognito sub>",
"Inputs": { "Type": "image_bundle", "Geography": "Pichincha", "GeographySource": "manual", "...": "..." }
}draft: Task created, editable, awaiting files (initial state)queued: Submitted; message placed on the inference queueprocessing: Inference workflow is runningcompleted: Processing finished successfullyfailed: Processing failedrequesting,canceled: reserved states defined in the model
Results are stored in the data lake as Parquet files. Access via:
- Athena queries
- Direct S3 access
- Task
Outputsfield contains file references
# Create conda environment
conda env create -f environment.yml
conda activate pavimentados
# Install dependencies
pip install -e .src/lambda/: Lambda function codesrc/sagemaker/: SageMaker processing codesrc/ecr/: Docker imagessrc/stepfunctions/: Workflow definitionssrc/apigateway/: API definitionstests/: Unit and functional tests
aws-lambda-powertools: Lambda utilitiesboto3: AWS SDKpavimentados: ML processing library (custom)pandas,pyarrow: Data processing