private static void ValidateJobProfile(JobProfile jobProfile, JobParameterBag jobInput)
        {
            if (jobProfile.Name != JOB_PROFILE_CONFORM_WORKFLOW && jobProfile.Name != JOB_PROFILE_AI_WORKFLOW)
            {
                throw new Exception("JobProfile '" + jobProfile.Name + "' is not supported");
            }

            if (jobProfile.InputParameters != null)
            {
                foreach (var parameter in jobProfile.InputParameters)
                {
                    if (!jobInput.HasProperty(parameter.ParameterName))
                    {
                        throw new Exception("jobInput misses required input parameter '" + parameter.ParameterName + "'");
                    }
                }
            }
        }
Ejemplo n.º 2
0
        private static void ValidateJobProfile(JobProfile jobProfile, JobParameterBag jobInput)
        {
            if (jobProfile.Name != JOB_PROFILE_TRANSCRIBE_AUDIO &&
                jobProfile.Name != JOB_PROFILE_TRANSLATE_TEXT &&
                jobProfile.Name != JOB_PROFILE_DETECT_CELEBRITIES)
            {
                throw new Exception("JobProfile '" + jobProfile.Name + "' is not supported");
            }

            if (jobProfile.InputParameters != null)
            {
                foreach (var parameter in jobProfile.InputParameters)
                {
                    if (!jobInput.HasProperty(parameter.ParameterName))
                    {
                        throw new Exception("jobInput misses required input parameter '" + parameter.ParameterName + "'");
                    }
                }
            }
        }
Ejemplo n.º 3
0
        private static async Task UpdateJobAssignmentWithOutputAsync(DynamoDbTable table, string jobAssignmentId, JobParameterBag jobOutput)
        {
            var jobAssignment = await GetJobAssignmentAsync(table, jobAssignmentId);

            jobAssignment.JobOutput = jobOutput;
            await PutJobAssignmentAsync(null, table, jobAssignmentId, jobAssignment);
        }
Ejemplo n.º 4
0
        internal static async Task ProcessJobAssignmentAsync(AwsAiServiceWorkerRequest @event)
        {
            var resourceManager = @event.GetAwsV4ResourceManager();
            var table           = new DynamoDbTable(@event.StageVariables["TableName"]);
            var jobAssignmentId = @event.JobAssignmentId;

            try
            {
                // 1. Setting job assignment status to RUNNING
                await UpdateJobAssignmentStatusAsync(resourceManager, table, jobAssignmentId, "RUNNING", null);

                // 2. Retrieving WorkflowJob
                var job = await RetrieveJobAsync(resourceManager, table, jobAssignmentId);

                // 3. Retrieve JobProfile
                var jobProfile = await RetrieveJobProfileAsync(resourceManager, job);

                // 4. Retrieve job inputParameters
                var jobInput = job.JobInput;

                // 5. Check if we support jobProfile and if we have required parameters in jobInput
                ValidateJobProfile(jobProfile, jobInput);

                S3Locator inputFile;
                if (!jobInput.TryGet <S3Locator>(nameof(inputFile), out inputFile))
                {
                    throw new Exception("Invalid or missing input file.");
                }

                S3Locator outputLocation;
                if (!jobInput.TryGet <S3Locator>(nameof(outputLocation), out outputLocation))
                {
                    throw new Exception("Invalid or missing output location.");
                }

                switch (jobProfile.Name)
                {
                case JOB_PROFILE_TRANSCRIBE_AUDIO:
                    string mediaFileUrl;
                    if (!string.IsNullOrWhiteSpace(inputFile.HttpEndpoint))
                    {
                        mediaFileUrl = inputFile.HttpEndpoint;
                    }
                    else
                    {
                        var bucketLocation = await inputFile.GetBucketLocationAsync();

                        var s3SubDomain = !string.IsNullOrWhiteSpace(bucketLocation) ? $"s3-{bucketLocation}" : "s3";
                        mediaFileUrl = $"https://{s3SubDomain}.amazonaws.com/{inputFile.AwsS3Bucket}/{inputFile.AwsS3Key}";
                    }

                    string mediaFormat;
                    if (mediaFileUrl.EndsWith("mp3", StringComparison.OrdinalIgnoreCase))
                    {
                        mediaFormat = "mp3";
                    }
                    else if (mediaFileUrl.EndsWith("mp4", StringComparison.OrdinalIgnoreCase))
                    {
                        mediaFormat = "mp4";
                    }
                    else if (mediaFileUrl.EndsWith("wav", StringComparison.OrdinalIgnoreCase))
                    {
                        mediaFormat = "wav";
                    }
                    else if (mediaFileUrl.EndsWith("flac", StringComparison.OrdinalIgnoreCase))
                    {
                        mediaFormat = "flac";
                    }
                    else
                    {
                        throw new Exception($"Unable to determine media format from input file '{mediaFileUrl}'");
                    }

                    var transcribeParameters = new StartTranscriptionJobRequest
                    {
                        TranscriptionJobName = "TranscriptionJob-" + jobAssignmentId.Substring(jobAssignmentId.LastIndexOf("/") + 1),
                        LanguageCode         = "en-US",
                        Media = new Media {
                            MediaFileUri = mediaFileUrl
                        },
                        MediaFormat      = mediaFormat,
                        OutputBucketName = @event.StageVariables["ServiceOutputBucket"]
                    };

                    var transcribeService = new AmazonTranscribeServiceClient();

                    var startJobResponse = await transcribeService.StartTranscriptionJobAsync(transcribeParameters);

                    Logger.Debug(startJobResponse.ToMcmaJson().ToString());
                    break;

                case JOB_PROFILE_TRANSLATE_TEXT:
                    var s3Bucket = inputFile.AwsS3Bucket;
                    var s3Key    = inputFile.AwsS3Key;

                    GetObjectResponse s3Object;
                    try
                    {
                        s3Object = await inputFile.GetAsync();
                    }
                    catch (Exception error)
                    {
                        throw new Exception($"Unable to read file in bucket '{s3Bucket}' with key '{s3Key}'.", error);
                    }

                    var inputText = await new StreamReader(s3Object.ResponseStream).ReadToEndAsync();

                    var translateParameters = new TranslateTextRequest
                    {
                        SourceLanguageCode = jobInput.TryGet("sourceLanguageCode", out string srcLanguageCode) ? srcLanguageCode : "auto",
                        TargetLanguageCode = jobInput.Get <string>("targetLanguageCode"),
                        Text = inputText
                    };

                    var translateService  = new AmazonTranslateClient();
                    var translateResponse = await translateService.TranslateTextAsync(translateParameters);

                    var s3Params = new PutObjectRequest
                    {
                        BucketName  = outputLocation.AwsS3Bucket,
                        Key         = (!string.IsNullOrWhiteSpace(outputLocation.AwsS3KeyPrefix) ? outputLocation.AwsS3Key : string.Empty) + Guid.NewGuid() + ".txt",
                        ContentBody = translateResponse.TranslatedText
                    };

                    var outputS3 = await outputLocation.GetClientAsync();

                    await outputS3.PutObjectAsync(s3Params);

                    var jobOutput = new JobParameterBag();
                    jobOutput["outputFile"] = new S3Locator
                    {
                        AwsS3Bucket = s3Params.BucketName,
                        AwsS3Key    = s3Params.Key
                    };

                    Logger.Debug("Updating job assignment");
                    await UpdateJobAssignmentWithOutputAsync(table, jobAssignmentId, jobOutput);
                    await UpdateJobAssignmentStatusAsync(resourceManager, table, jobAssignmentId, "COMPLETED");

                    break;

                case JOB_PROFILE_DETECT_CELEBRITIES:
                    var randomBytes = new byte[16];
                    new Random().NextBytes(randomBytes);
                    var clientToken = randomBytes.HexEncode();

                    var base64JobId = Encoding.UTF8.GetBytes(jobAssignmentId).HexEncode();

                    var rekoParams = new StartCelebrityRecognitionRequest
                    {
                        Video = new Video
                        {
                            S3Object = new Amazon.Rekognition.Model.S3Object
                            {
                                Bucket = inputFile.AwsS3Bucket,
                                Name   = inputFile.AwsS3Key
                            }
                        },
                        ClientRequestToken  = clientToken,
                        JobTag              = base64JobId,
                        NotificationChannel = new NotificationChannel
                        {
                            RoleArn     = REKO_SNS_ROLE_ARN,
                            SNSTopicArn = SNS_TOPIC_ARN
                        }
                    };

                    var rekognitionClient             = new AmazonRekognitionClient();
                    var startCelebRecognitionResponse = await rekognitionClient.StartCelebrityRecognitionAsync(rekoParams);

                    Logger.Debug(startCelebRecognitionResponse.ToMcmaJson().ToString());
                    break;
                }
            }
            catch (Exception ex)
            {
                Logger.Exception(ex);

                try
                {
                    await UpdateJobAssignmentStatusAsync(resourceManager, table, jobAssignmentId, "FAILED", ex.ToString());
                }
                catch (Exception innerEx)
                {
                    Logger.Exception(innerEx);
                }
            }
        }
Ejemplo n.º 5
0
        public static async Task ProcessRekognitionResultAsync(AwsAiServiceWorkerRequest @event)
        {
            var resourceManager = @event.GetAwsV4ResourceManager();
            var table           = new DynamoDbTable(@event.StageVariables["TableName"]);
            var jobAssignmentId = @event.JobAssignmentId;

            var job = await RetrieveJobAsync(resourceManager, table, jobAssignmentId);

            try
            {
                var jobInput = job.JobInput;

                S3Locator outputLocation;
                if (!jobInput.TryGet <S3Locator>(nameof(outputLocation), out outputLocation))
                {
                    throw new Exception("Invalid or missing output location.");
                }

                var s3Bucket = outputLocation.AwsS3Bucket;

                var rekoJobId   = @event.JobExternalInfo.RekoJobId;
                var rekoJobType = @event.JobExternalInfo.RekoJobType;
                var status      = @event.JobExternalInfo.Status;

                if (status != "SUCCEEDED")
                {
                    throw new Exception($"AI Rekognition failed job info: rekognition status:" + status);
                }

                object data = null;
                switch (rekoJobType)
                {
                case "StartCelebrityRecognition":
                    var getCelebRecognitionParams = new GetCelebrityRecognitionRequest
                    {
                        JobId      = rekoJobId, /* required */
                        MaxResults = 1000000,
                        SortBy     = "TIMESTAMP"
                    };

                    var rekognitionClient = new AmazonRekognitionClient();
                    data = await rekognitionClient.GetCelebrityRecognitionAsync(getCelebRecognitionParams);

                    break;

                case "StartLabelDetection":
                    throw new NotImplementedException("StartLabelDetection has not yet been implemented");

                case "StartContentModeration":
                    throw new NotImplementedException("StartContentModeration has not yet been implemented");

                case "StartPersonTracking":
                    throw new NotImplementedException("StartPersonTracking has not yet been implemented");

                case "StartFaceDetection":
                    throw new NotImplementedException("StartLabelDetection has not yet been implemented");

                case "StartFaceSearch":
                    throw new NotImplementedException("StartLabelDetection has not yet been implemented");

                default:
                    throw new Exception($"Unknown job type '{rekoJobType}'");
                }

                if (data == null)
                {
                    throw new Exception($"No data was returned by AWS Rekognition");
                }

                var newS3Key = $"reko_{Guid.NewGuid()}.json";
                var s3Params = new PutObjectRequest
                {
                    BucketName  = outputLocation.AwsS3Bucket,
                    Key         = newS3Key,
                    ContentBody = data.ToMcmaJson().ToString(),
                    ContentType = "application/json"
                };

                try
                {
                    var destS3 = await outputLocation.GetClientAsync();

                    await destS3.PutObjectAsync(s3Params);
                }
                catch (Exception error)
                {
                    Logger.Error("Unable to write output file to bucket '" + s3Bucket + "' with key '" + newS3Key + "'");
                    Logger.Exception(error);
                }

                var jobOutput = new JobParameterBag();
                jobOutput["outputFile"] = new S3Locator
                {
                    AwsS3Bucket = s3Bucket,
                    AwsS3Key    = newS3Key
                };

                await UpdateJobAssignmentWithOutputAsync(table, jobAssignmentId, jobOutput);
                await UpdateJobAssignmentStatusAsync(resourceManager, table, jobAssignmentId, "COMPLETED");
            }
            catch (Exception error)
            {
                Logger.Exception(error);

                try
                {
                    await UpdateJobAssignmentStatusAsync(resourceManager, table, jobAssignmentId, "FAILED", error.ToString());
                }
                catch (Exception innerError)
                {
                    Logger.Exception(innerError);
                }
            }
        }
Ejemplo n.º 6
0
        public static async Task ProcessTranscribeJobResultAsync(AwsAiServiceWorkerRequest @event)
        {
            var resourceManager = @event.GetAwsV4ResourceManager();
            var table           = new DynamoDbTable(@event.StageVariables["TableName"]);
            var jobAssignmentId = @event.JobAssignmentId;

            var job = await RetrieveJobAsync(resourceManager, table, jobAssignmentId);

            try
            {
                var jobInput = job.JobInput;

                S3Locator outputLocation;
                if (!jobInput.TryGet <S3Locator>(nameof(outputLocation), out outputLocation))
                {
                    throw new Exception("Invalid or missing output location.");
                }

                var copySource = Uri.EscapeDataString(@event.OutputFile.AwsS3Bucket + "/" + @event.OutputFile.AwsS3Key);

                var s3Bucket = outputLocation.AwsS3Bucket;
                var s3Key    = (!string.IsNullOrWhiteSpace(outputLocation.AwsS3KeyPrefix) ? outputLocation.AwsS3KeyPrefix : string.Empty) + @event.OutputFile.AwsS3Key;

                try
                {
                    var destS3 = await outputLocation.GetClientAsync();

                    await destS3.CopyObjectAsync(new CopyObjectRequest
                    {
                        SourceBucket      = @event.OutputFile.AwsS3Bucket,
                        SourceKey         = @event.OutputFile.AwsS3Key,
                        DestinationBucket = s3Bucket,
                        DestinationKey    = s3Key
                    });
                }
                catch (Exception error)
                {
                    throw new Exception("Unable to copy output file to bucket '" + s3Bucket + "' with key'" + s3Key + "'", error);
                }

                var jobOutput = new JobParameterBag();
                jobOutput["outputFile"] = new S3Locator
                {
                    AwsS3Bucket = s3Bucket,
                    AwsS3Key    = s3Key
                };

                await UpdateJobAssignmentWithOutputAsync(table, jobAssignmentId, jobOutput);
                await UpdateJobAssignmentStatusAsync(resourceManager, table, jobAssignmentId, "COMPLETED");
            }
            catch (Exception error)
            {
                Logger.Exception(error);

                try
                {
                    await UpdateJobAssignmentStatusAsync(resourceManager, table, jobAssignmentId, "FAILED", error.ToString());
                }
                catch (Exception innerError)
                {
                    Logger.Exception(innerError);
                }
            }

            // Cleanup: Deleting original output file
            try
            {
                var sourceS3 = await @event.OutputFile.GetClientAsync();

                await sourceS3.DeleteObjectAsync(new DeleteObjectRequest
                {
                    BucketName = @event.OutputFile.AwsS3Bucket,
                    Key        = @event.OutputFile.AwsS3Key,
                });
            }
            catch (Exception error)
            {
                Logger.Error("Failed to cleanup transcribe output file.");
                Logger.Exception(error);
            }
        }
        internal static async Task ProcessJobAssignmentAsync(AzureAiServiceWorkerRequest @event)
        {
            var resourceManager = @event.GetAwsV4ResourceManager();
            var table           = new DynamoDbTable(@event.StageVariables["TableName"]);
            var jobAssignmentId = @event.JobAssignmentId;
            var azure           = new AzureConfig(@event);

            try
            {
                // 1. Setting job assignment status to RUNNING
                await UpdateJobAssignmentStatusAsync(resourceManager, table, jobAssignmentId, "RUNNING", null);

                // 2. Retrieving WorkflowJob
                var job = await RetrieveJobAsync(resourceManager, table, jobAssignmentId);

                // 3. Retrieve JobProfile
                var jobProfile = await RetrieveJobProfileAsync(resourceManager, job);

                // 4. Retrieve job inputParameters
                var jobInput = job.JobInput;

                // 5. Check if we support jobProfile and if we have required parameters in jobInput
                ValidateJobProfile(jobProfile, jobInput);

                S3Locator inputFile;
                if (!jobInput.TryGet <S3Locator>(nameof(inputFile), out inputFile))
                {
                    throw new Exception("Invalid or missing input file.");
                }

                string mediaFileUrl;
                if (!string.IsNullOrWhiteSpace(inputFile.HttpEndpoint))
                {
                    mediaFileUrl = inputFile.HttpEndpoint;
                }
                else
                {
                    var bucketLocation = await inputFile.GetBucketLocationAsync();

                    var s3SubDomain = !string.IsNullOrWhiteSpace(bucketLocation) ? $"s3-{bucketLocation}" : "s3";
                    mediaFileUrl = $"https://{s3SubDomain}.amazonaws.com/{inputFile.AwsS3Bucket}/{inputFile.AwsS3Key}";
                }

                switch (jobProfile.Name)
                {
                case JOB_PROFILE_TRANSCRIBE_AUDIO:
                case JOB_PROFILE_TRANSLATE_TEXT:
                    throw new NotImplementedException($"{jobProfile.Name} profile has not yet been implemented for Azure.");

                case JOB_PROFILE_EXTRACT_ALL_AI_METADATA:
                    var authTokenUrl  = azure.ApiUrl + "/auth/" + azure.Location + "/Accounts/" + azure.AccountID + "/AccessToken?allowEdit=true";
                    var customHeaders = new Dictionary <string, string> {
                        ["Ocp-Apim-Subscription-Key"] = azure.SubscriptionKey
                    };

                    Logger.Debug($"Generate Azure Video Indexer Token: Doing a GET on {authTokenUrl}");
                    var mcmaHttp = new McmaHttpClient();
                    var response = await mcmaHttp.GetAsync(authTokenUrl, headers : customHeaders).WithErrorHandling();

                    var apiToken = await response.Content.ReadAsJsonAsync();

                    Logger.Debug($"Azure API Token: {apiToken}");

                    // call the Azure API to process the video
                    // in this scenario the video is located in a public link
                    // so no need to upload the file to Azure

                    /* Sample URL Structure
                     *  https://api.videoindexer.ai/{location}/Accounts/{accountId}/Videos?
                     *      accessToken={accessToken}&
                     *      name={name}?description={string}&
                     *      partition={string}&
                     *      externalId={string}&
                     *      callbackUrl={string}&
                     *      metadata={string}&
                     *      language={string}&
                     *      videoUrl={string}&
                     *      fileName={string}&
                     *      indexingPreset={string}&
                     *      streamingPreset=Default&
                     *      linguisticModelId={string}&
                     *      privacy={string}&
                     *      externalUrl={string}" */

                    var secureHost    = new Uri(jobAssignmentId, UriKind.Absolute).Host;
                    var nonSecureHost = new Uri(@event.StageVariables["PublicUrlNonSecure"], UriKind.Absolute).Host;

                    var callbackUrl = Uri.EscapeDataString(jobAssignmentId.Replace(secureHost, nonSecureHost) + "/notifications");

                    var postVideoUrl = azure.ApiUrl + "/" + azure.Location + "/Accounts/" + azure.AccountID + "/Videos?accessToken=" + apiToken + "&name=" + inputFile.AwsS3Key + "&callbackUrl=" + callbackUrl + "&videoUrl=" + mediaFileUrl + "&fileName=" + inputFile.AwsS3Key;

                    Logger.Debug($"Call Azure Video Indexer API: Doing a POST on {postVideoUrl}");
                    var postVideoResponse = await mcmaHttp.PostAsync(postVideoUrl, null, customHeaders).WithErrorHandling();

                    var azureAssetInfo = await postVideoResponse.Content.ReadAsJsonAsync();

                    Logger.Debug("azureAssetInfo: ", azureAssetInfo);

                    try
                    {
                        var jobOutput = new JobParameterBag();
                        jobOutput["jobInfo"] = azureAssetInfo;

                        await UpdateJobAssignmentWithOutputAsync(table, jobAssignmentId, jobOutput);
                    }
                    catch (Exception error)
                    {
                        Logger.Error("Error updating the job", error);
                    }

                    break;
                }
            }
            catch (Exception ex)
            {
                Logger.Exception(ex);

                try
                {
                    await UpdateJobAssignmentStatusAsync(resourceManager, table, jobAssignmentId, "FAILED", ex.ToString());
                }
                catch (Exception innerEx)
                {
                    Logger.Exception(innerEx);
                }
            }
        }
Ejemplo n.º 8
0
        internal static async Task ProcessJobAssignmentAsync(TransformServiceWorkerRequest @event)
        {
            var resourceManager = @event.Request.GetAwsV4ResourceManager();
            var table           = new DynamoDbTable(@event.Request.StageVariables["TableName"]);
            var jobAssignmentId = @event.JobAssignmentId;

            try
            {
                // 1. Setting job assignment status to RUNNING
                await UpdateJobAssignmentStatusAsync(resourceManager, table, jobAssignmentId, "RUNNING", null);

                // 2. Retrieving WorkflowJob
                var transformJob = await RetrieveTransformJobAsync(resourceManager, table, jobAssignmentId);

                // 3. Retrieve JobProfile
                var jobProfile = await RetrieveJobProfileAsync(resourceManager, transformJob);

                // 4. Retrieve job inputParameters
                var jobInput = transformJob.JobInput;

                // 5. Check if we support jobProfile and if we have required parameters in jobInput
                ValidateJobProfile(jobProfile, jobInput);

                switch (jobProfile.Name)
                {
                case JOB_PROFILE_CREATE_PROXY_LAMBDA:
                    S3Locator inputFile;
                    if (!jobInput.TryGet <S3Locator>(nameof(inputFile), out inputFile))
                    {
                        throw new Exception("Invalid or missing input file.");
                    }

                    S3Locator outputLocation;
                    if (!jobInput.TryGet <S3Locator>(nameof(outputLocation), out outputLocation))
                    {
                        throw new Exception("Invalid or missing output location.");
                    }

                    if (string.IsNullOrWhiteSpace(inputFile.AwsS3Bucket) || string.IsNullOrWhiteSpace(inputFile.AwsS3Key))
                    {
                        throw new Exception("Not able to obtain input file");
                    }

                    var data = await inputFile.GetAsync();

                    var localFileName = "/tmp/" + Guid.NewGuid();
                    await data.WriteResponseStreamToFileAsync(localFileName, true, CancellationToken.None);

                    var tempFileName  = "/tmp/" + Guid.NewGuid() + ".mp4";
                    var ffmpegParams  = new[] { "-y", "-i", localFileName, "-preset", "ultrafast", "-vf", "scale=-1:360", "-c:v", "libx264", "-pix_fmt", "yuv420p", tempFileName };
                    var ffmpegProcess = await FFmpegProcess.RunAsync(ffmpegParams);

                    File.Delete(localFileName);

                    var s3Params = new PutObjectRequest
                    {
                        BucketName  = outputLocation.AwsS3Bucket,
                        Key         = (outputLocation.AwsS3KeyPrefix ?? string.Empty) + Guid.NewGuid().ToString() + ".mp4",
                        FilePath    = tempFileName,
                        ContentType = "video/mp4"
                    };

                    var outputS3 = await outputLocation.GetClientAsync();

                    var putResp = await outputS3.PutObjectAsync(s3Params);

                    var jobOutput = new JobParameterBag();
                    jobOutput["outputFile"] = new S3Locator
                    {
                        AwsS3Bucket = s3Params.BucketName,
                        AwsS3Key    = s3Params.Key
                    };

                    await UpdateJobAssignmentWithOutputAsync(table, jobAssignmentId, jobOutput);
                    await UpdateJobAssignmentStatusAsync(resourceManager, table, jobAssignmentId, "COMPLETED");

                    break;

                case JOB_PROFILE_CREATE_PROXY_EC2:
                    var ec2hostname = @event.Request.StageVariables["HostnameInstanceEC2"];

                    var ec2Url = "http://" + ec2hostname + "/new-transform-job";

                    var message = new
                    {
                        input = jobInput,
                        notificationEndpoint = new NotificationEndpoint {
                            HttpEndpoint = jobAssignmentId + "/notifications"
                        }
                    };

                    Logger.Debug("Sending to", ec2Url, "message", message);
                    var mcmaHttp = new McmaHttpClient();
                    await mcmaHttp.PostAsJsonAsync(ec2Url, message);

                    Logger.Debug("Done");
                    break;
                }
            }
            catch (Exception ex)
            {
                Logger.Exception(ex);

                try
                {
                    await UpdateJobAssignmentStatusAsync(resourceManager, table, jobAssignmentId, "FAILED", ex.ToString());
                }
                catch (Exception innerEx)
                {
                    Logger.Exception(innerEx);
                }
            }
            finally
            {
                Logger.Debug("Exiting TransformServiceWorker.ProcessJobAssignmentAsync");
            }
        }