📄 src/Domain/ClipDescriber.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

namespace Slopper.Domain;

public sealed class ClipDescriber(
    IFrameExtractor frameExtractor,
    ISubtitleReader subtitleReader,
    IChatClient chatClient,
    IOptionsMonitor<ClipDescriberOptions> options
)
{
    public async Task<string> 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)],
            cancellationToken: cancellationToken
        );

        return response.Text;
    }
}

public sealed class ClipDescriberOptions
{
    [Required]
    public required string Prompt { get; set; }
}

[OptionsValidator]
internal sealed partial class ClipDescriberOptionsValidator : IValidateOptions<ClipDescriberOptions>;

public static class ClipDescriberServiceCollectionExtensions
{
    extension(IServiceCollection services)
    {
        public IServiceCollection AddClipDescriber()
        {
            services.AddOptions<ClipDescriberOptions>().BindConfiguration("ClipDescriber").ValidateOnStart();
            services.AddTransient<IValidateOptions<ClipDescriberOptions>, ClipDescriberOptionsValidator>();
            services.AddTransient<ClipDescriber>();

            return services;
        }
    }
}