From a01d2cd266e96386b55476ff39af8da55b533c9c Mon Sep 17 00:00:00 2001 From: Ayrat Hudaygulov Date: Mon, 5 Oct 2026 11:11:40 +0100 Subject: [PATCH] Overlap Azure OCR with local barcode decoding --- src/CouponHubBot/Services/CouponOcrEngine.fs | 11 ++-- tests/CouponHubBot.Ocr.Tests/OcrTests.fs | 60 ++++++++++++++++++++ 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/src/CouponHubBot/Services/CouponOcrEngine.fs b/src/CouponHubBot/Services/CouponOcrEngine.fs index 6fbed1c..ceb172a 100644 --- a/src/CouponHubBot/Services/CouponOcrEngine.fs +++ b/src/CouponHubBot/Services/CouponOcrEngine.fs @@ -455,14 +455,9 @@ type CouponOcrEngine(azureTextOcr: IBotOcr, logger: ILogger, ti if not (isNull activity) then %activity.SetTag("image.size_bytes", imageBytes.Length) let nowUtc = time.GetUtcNow().UtcDateTime - let barcode = tryDecodeBarcode imageBytes - // The Azure OCR SDK already retried transient failures; if one still surfaces (network, - // timeout, or a non-2xx response after retries) AnalyzeImageBytes re-throws it. Catch it - // here (don't crash) and record that the OCR *backend* failed, so callers can tell "Azure - // was down/timed out/errored" apart from "Azure answered 200 but found no usable text". - // The ZXing barcode above is independent of Azure and may still be set. - let! ocrAnalysis, backendFailed = + // OCR runs while the local barcode decoder works; backend failure stays separate from empty text. + let ocrTask = task { use azure = botActivity.StartActivity("couponOcr.azure") try @@ -476,6 +471,8 @@ type CouponOcrEngine(azureTextOcr: IBotOcr, logger: ILogger, ti return (null: OcrAnalysis | null), true } + let barcode = tryDecodeBarcode imageBytes + let! ocrAnalysis, backendFailed = ocrTask let ocrText = if isNull ocrAnalysis then null else ocrAnalysis.Text use parse = botActivity.StartActivity("couponOcr.parse") diff --git a/tests/CouponHubBot.Ocr.Tests/OcrTests.fs b/tests/CouponHubBot.Ocr.Tests/OcrTests.fs index 2fbb395..187c8c2 100644 --- a/tests/CouponHubBot.Ocr.Tests/OcrTests.fs +++ b/tests/CouponHubBot.Ocr.Tests/OcrTests.fs @@ -302,6 +302,66 @@ type OcrTests(output: ITestOutputHelper) = do! Task.WhenAll jobs } + [] + [] + [] + [] + [] + member _.``Recognition awaits pending OCR and preserves the local barcode``(outcome: string) = + task { + let bytes = File.ReadAllBytes(Path.Combine(AppContext.BaseDirectory, "Images", "5_25_2026-02-08_2026-02-14_2706726228947.jpg")) + let completion = TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously) + let mutable requests = 0 + let azure = + { new IBotOcr with + member _.AnalyzeImageBytes _ = + requests <- requests + 1 + completion.Task } + let time = BotInfra.Time.FixedTimeProvider(DateTimeOffset(2026, 2, 8, 0, 0, 0, TimeSpan.Zero)) + let engine = CouponOcrEngine(azure, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance, time) + let recognition = engine.Recognize(ReadOnlyMemory(bytes)) + Assert.Equal(1, requests) + Assert.False(recognition.IsCompleted) + + match outcome with + | "success" -> + completion.SetResult( + { RawJson = "{}" + Text = "€5 OFF €25\nValid 8 Feb - 14 Feb\n2706602781191" }) + | "empty" -> completion.SetResult(null: OcrAnalysis | null) + | "failure" -> completion.SetException(InvalidOperationException("OCR unavailable")) + | "canceled" -> completion.SetCanceled() + | _ -> failwith "Unexpected OCR outcome" + + let! result = recognition + Assert.Equal("2706726228947", result.barcode) + Assert.Equal(outcome = "failure" || outcome = "canceled", result.backendFailed) + match outcome with + | "success" -> + Assert.Equal(Nullable 5m, result.couponValue) + Assert.Equal(Nullable 25m, result.minCheck) + Assert.Equal(Nullable(DateTime(2026, 2, 8)), result.validFrom) + Assert.Equal(Nullable(DateTime(2026, 2, 14)), result.validTo) + | _ -> + Assert.False(result.couponValue.HasValue) + Assert.False(result.minCheck.HasValue) + Assert.False(result.validFrom.HasValue) + Assert.False(result.validTo.HasValue) + } + + [] + member _.``Immediate OCR failure preserves the local barcode``() = + task { + let bytes = File.ReadAllBytes(Path.Combine(AppContext.BaseDirectory, "Images", "5_25_2026-02-08_2026-02-14_2706726228947.jpg")) + let azure = + { new IBotOcr with + member _.AnalyzeImageBytes _ = raise (InvalidOperationException("OCR unavailable")) } + let engine = CouponOcrEngine(azure, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance, TimeProvider.System) + let! result = engine.Recognize(ReadOnlyMemory(bytes)) + Assert.Equal("2706726228947", result.barcode) + Assert.True(result.backendFailed) + } + [] [] []