📄 src/Domain/Describer/ClipDescriber.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Options;

namespace Slopper.Domain.Describer;

public sealed class ClipDescriber(
    IFrameExtractor frameExtractor,
    ISubtitleReader subtitleReader,
    IChatClient chatClient,
    IOptionsMonitor<ClipDescriberOptions> options
)
{
    public async Task<ClipDescription> DescribeClip(
        MediaItem media,
        TimeSpan start,
        TimeSpan duration,
        CancellationToken cancellationToken
    )
    {
        var frames = await frameExtractor.ExtractFrames(media, [start, start + duration], cancellationToken);
        var subtitles = await subtitleReader.ReadSubtitles(media, start, duration, cancellationToken);

        List<AIContent> contents = [];
        foreach (var frame in frames)
        {
            contents.Add(new DataContent(frame, "image/png"));
        }
        contents.Add(new TextContent(options.CurrentValue.Prompt));
        if (subtitles.Count > 0)
        {
            contents.Add(
                new TextContent(
                    $"Subtitles from this clip:\n{string.Join("\n", subtitles.Select(s => string.Join(" / ", s.Lines)))}"
                )
            );
        }

        var response = await chatClient.GetResponseAsync(
            [new ChatMessage(ChatRole.User, contents)],
            new()
            {
                ResponseFormat = ChatResponseFormat.ForJsonSchema<ClipDescriptionAiDto>(
                    AiDtoSerializerContext.Default.Options
                ),
            },
            cancellationToken
        );

        var result =
            JsonSerializer.Deserialize(response.Text, AiDtoSerializerContext.Default.ClipDescriptionAiDto)
            ?? throw new Exception("Literal null response from description agent.");

        return new(result.Caption, result.Tags.Select(t => new Tag(t)).ToHashSet());
    }
}