Search code examples
swiftamazon-s3aws-lambdaamazon-dynamodbsoto

Use AWS Rekognition in Swift SOTO


I have used the devhr python lambda function as a model for my Swift Lambda function using Soto for AWS. The Lambda function will be triggered by an S3 event when an image is uploaded to the S3, and using the AWS Rekognition service to add labels (description of objects in the image) to a DynamoDB database. I have problems getting the rekFunction function correct, and I hope someone can advice how to make it work.

The Swift code I have made so far:

import AWSLambdaRuntime
import AWSLambdaEvents
import NIO
import Foundation
import SotoS3
import SotoRekognition
import SotoDynamoDB

struct Bucket: Decodable {
    let name: String
}

struct Object: Decodable {
    let key: String
}

struct MyS3: Decodable {
    let bucket: Bucket
    let object: Object
}

struct Record: Decodable {
    let s3: MyS3
}

struct Input: Decodable {
    let records: [Record]
}

struct Output: Encodable {
    let result: String
}

struct MyHandler: EventLoopLambdaHandler {
    typealias In = APIGateway.V2.Request
    typealias Out = APIGateway.V2.Response
    
    let minConfidence: Float = 50

    let awsClient: AWSClient
    
    init(context: Lambda.InitializationContext) {
        self.awsClient = AWSClient(httpClientProvider: .createNewWithEventLoopGroup(context.eventLoop))
    }
    
    func shutdown(context: Lambda.ShutdownContext) -> EventLoopFuture<Void> {
        let promise = context.eventLoop.makePromise(of: Void.self)
        awsClient.shutdown { error in
            if let error = error {
                promise.fail(error)
            } else {
                promise.succeed(())
            }
        }
        return context.eventLoop.makeSucceededFuture(())
    }

    func handle(context: Lambda.Context, event: In) -> EventLoopFuture<Out> {
        guard let input: Input = try? event.bodyObject() else {
            return context.eventLoop.makeSucceededFuture(APIGateway.V2.Response(with: APIError.requestError, statusCode: .badRequest))
        }

        for record in input.records {
            let ourBucket = record.s3.bucket.name
            let ourKey = record.s3.object.key
            
            // For each message (photo) get the bucket name and key
            rekFunction(bucket: ourBucket, key: ourKey)
        }
        let output = Output(result: "Finished!")
        let apigatewayOutput = APIGateway.V2.Response(with: output, statusCode: .ok)
        return context.eventLoop.makeSucceededFuture(apigatewayOutput)
    }
        
    func rekFunction(bucket: String, key: String) {
        let safeKey = key.replacingOccurrences(of: "%3A", with: ":")
        print("Currently processing the following image")
        print("Bucket:", bucket, " key name:", safeKey)
        var objectsDetected: [String] = []
        var imageLabels = [ "image": safeKey ]
        let s3Client = S3(client: awsClient, region: .euwest1)

        let s3Object = Rekognition.S3Object(bucket: bucket, name: safeKey)
        let image = Rekognition.Image(s3Object: s3Object)
        let rekognitionClient = Rekognition(client: awsClient)
        let detectLabelsRequest = Rekognition.DetectLabelsRequest(image: image, maxLabels: 10, minConfidence: minConfidence)
        rekognitionClient.detectLabels(detectLabelsRequest)
            .flatMap { detectLabelsResponse -> EventLoopFuture<Void> in
                if let labels = detectLabelsResponse.labels {
                    // Add all of our labels into imageLabels by iterating over response['Labels']
                    for label in labels {
                        if let name = label.name {
                            objectsDetected.append(name)
                            let itemAtt = "object\(objectsDetected.count)"
                            
                            // We now have our shiny new item ready to put into DynamoDB
                            imageLabels[itemAtt] = name
                            
                            // Instantiate a table resource object of our environment variable
                            let imageLabelsTable = // Environment("TABLE") How can I read env vars?
                            let table = SotoDynamoDB.getTable(imageLabelsTable) // python: table = dynamodb.Table(imageLabelsTable)
                            // python: table.put_item(Item=imageLabels)
                        }
                    }
                }
                return ???
            }
        
    }
    
}
Lambda.run { MyHandler(context: $0) }

Solution

  • There are a number of issues to cover here.

    1. As you are triggering this from an S3 Event. The MyLambda.In typealias should be S3.Event and given there isn't anything waiting on the results MyLambda.Out should be Void.
    2. You can extract environment variables using Lambda.env("TABLE").
    3. As for writing into your DynamoDB table the best way is probably to use the Soto DynamoDB Codable support. In the example below I will assume your are use a combination of your s3 bucket and key as the key.
    struct RekEntry: Codable {
        let key: String
        let labels: [String]
    }
    let entry = RekEntry(
        key: "\(bucket)/\(key)"
        labels: labels
    )
    let putRequest = DynamoDB.PutItemCodableInput(
        item: entry,
        tableName: Lambda.env("TABLE")
    )
    return dynamoDB.putItem(putRequest)
    
    1. Finally what to return from your flatMap. First your function rekFunction needs to return an EventLoopFuture<Void> as this is what you are returning from the Lambda handler. So you would map the result of the dynamoDB.putItem in the previous point to Void.
    func rekFunction(bucket: String, key: String) -> EventLoopFuture<Void> {
        ...
        return rekognitionClient.detectLabels(detectLabelsRequest)
            .flatMap {
                ...
                return dynamoDB.putItem(putRequest)
            }
            .map { _ in }
    }
    

    I hope that covers everything