< Summary

Information
Class: Chronicis.Api.Services.TranscriptionService
Assembly: Chronicis.Api
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Api/Services/TranscriptionService.cs
Line coverage
100%
Covered lines: 18
Uncovered lines: 0
Coverable lines: 18
Total lines: 114
Line coverage: 100%
Branch coverage
100%
Covered branches: 8
Total branches: 8
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%
.ctor(...)100%88100%
.ctor(...)100%11100%

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Api/Services/TranscriptionService.cs

#LineLine coverage
 1using Azure;
 2using Azure.AI.OpenAI;
 3using Chronicis.Shared.DTOs;
 4using Chronicis.Shared.Extensions;
 5using OpenAI.Chat;
 6
 7namespace Chronicis.Api.Services;
 8
 9/// <summary>
 10/// Transcription service that uses Azure OpenAI GPT-4 Vision to convert handwritten note images to text.
 11/// </summary>
 12public sealed class TranscriptionService : ITranscriptionService
 13{
 14    private readonly ChatClient _chatClient;
 15    private readonly ILogger<TranscriptionService> _logger;
 16    private readonly TimeSpan _timeout;
 117    internal static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(60);
 18
 19    public TranscriptionService(IConfiguration configuration, ILogger<TranscriptionService> logger)
 420        : this(configuration, logger, DefaultTimeout)
 21    {
 122    }
 23
 24    internal TranscriptionService(IConfiguration configuration, ILogger<TranscriptionService> logger, TimeSpan timeout)
 25    {
 426        _logger = logger;
 427        _timeout = timeout;
 28
 429        var endpoint = configuration["AzureOpenAI:Endpoint"];
 430        var apiKey = configuration["AzureOpenAI:ApiKey"];
 431        var deploymentName = configuration["AzureOpenAI:VisionDeploymentName"]
 432            ?? configuration["AzureOpenAI:DeploymentName"];
 33
 434        if (string.IsNullOrEmpty(endpoint) || string.IsNullOrEmpty(apiKey) || string.IsNullOrEmpty(deploymentName))
 35        {
 336            throw new InvalidOperationException("AzureOpenAI configuration is incomplete for transcription.");
 37        }
 38
 139        var client = new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(apiKey));
 140        _chatClient = client.GetChatClient(deploymentName);
 141    }
 42
 43    // Test constructor
 44    internal TranscriptionService(ChatClient chatClient, ILogger<TranscriptionService> logger, TimeSpan timeout)
 45    {
 246        _chatClient = chatClient;
 247        _logger = logger;
 248        _timeout = timeout;
 249    }
 50
 51    public async Task<TranscriptionResultDto> TranscribeImageAsync(byte[] imageBytes, CancellationToken cancellationToke
 52    {
 53        using var timeoutCts = new CancellationTokenSource(_timeout);
 54        using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, cancellationToken);
 55
 56        try
 57        {
 58            var imageData = BinaryData.FromBytes(imageBytes);
 59
 60            var messages = new List<ChatMessage>
 61            {
 62                new SystemChatMessage("You are a transcription assistant. Transcribe the handwritten text in the image e
 63                new UserChatMessage(
 64                    ChatMessageContentPart.CreateTextPart("Transcribe the handwritten text in this image:"),
 65                    ChatMessageContentPart.CreateImagePart(imageData, "image/png"))
 66            };
 67
 68            var options = new ChatCompletionOptions
 69            {
 70                MaxOutputTokenCount = 4000
 71            };
 72
 73            var completion = await _chatClient.CompleteChatAsync(messages, options, linkedCts.Token);
 74            var text = completion.Value.Content[0].Text?.Trim() ?? string.Empty;
 75
 76            if (string.IsNullOrWhiteSpace(text))
 77            {
 78                return new TranscriptionResultDto
 79                {
 80                    Success = false,
 81                    ErrorMessage = "Transcription produced no text."
 82                };
 83            }
 84
 85            return new TranscriptionResultDto
 86            {
 87                Success = true,
 88                Text = text
 89            };
 90        }
 91        catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested && !cancellationToken.IsCancellation
 92        {
 93            _logger.LogErrorSanitized("Transcription request timed out after {Timeout} seconds", _timeout.TotalSeconds);
 94            return new TranscriptionResultDto
 95            {
 96                Success = false,
 97                ErrorMessage = $"Transcription timed out after {(int)_timeout.TotalSeconds} seconds."
 98            };
 99        }
 100        catch (OperationCanceledException)
 101        {
 102            throw;
 103        }
 104        catch (Exception ex)
 105        {
 106            _logger.LogErrorSanitized(ex, "Transcription failed");
 107            return new TranscriptionResultDto
 108            {
 109                Success = false,
 110                ErrorMessage = "Transcription service failed."
 111            };
 112        }
 113    }
 114}