src/Api/appsettings.Development.json
+3
-1
diff --git a/src/Api/appsettings.Development.json b/src/Api/appsettings.Development.json
index 69ec8dc..0ac4374 100644
@@ -16,7 +16,9 @@
"PublishInterval": "00:00:01"
},
"ClipDescriber": {
"Prompt": "Give one short sentence caption and tags for the attached frames from a video clip, optimized for engagement on TikTok and Instagram Reels. Answer with JSON, one string field for `caption` and an array of strings for `tags`. Make the caption one sentence only optimized for engagement, and the tags one word or camel case without the hashtag symbol."
"Prompt": "Give one short sentence caption and tags for the attached frames from a video clip, optimized for engagement on TikTok and Instagram Reels. Answer with JSON, one string field for `caption` and an array of strings for `tags`. Make the caption one sentence only optimized for engagement, and the tags one word or camel case without the hashtag symbol.",
"MovieMetadataPrompt": "The clip comes from {0} released on {1}.",
"ShowMetadataPrompt": "The clip comes from episode {0} of {1} released on {2}."
},
"ClipSelector": {
"ClippableQuotes": [
src/Cli/Program.cs
+8
-8
diff --git a/src/Cli/Program.cs b/src/Cli/Program.cs
index 42bb2b1..f0bc3c4 100644
@@ -1,11 +1,12 @@
using System;
using System.IO;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Slopper.Cli;
using Slopper.Cli.YouTubeAuth;
using Slopper.Domain;
using Slopper.Domain.Describer;
using Slopper.Infrastructure.Ai;
using Slopper.Infrastructure.Database;
using Slopper.Infrastructure.Ffmpeg;
@@ -30,17 +31,16 @@ var logger = app.Services.GetRequiredService<ILogger<Program>>();
using var scope = app.Services.CreateScope();
var timeProvider = scope.ServiceProvider.GetRequiredService<TimeProvider>();
var clipExtractor = scope.ServiceProvider.GetRequiredService<IClipExtractor>();
var clipDescriber = scope.ServiceProvider.GetRequiredService<ClipDescriber>();
var utcNow = timeProvider.GetUtcNow();
var media = new MediaItem(Guid.CreateVersion7(utcNow), args[0], new Subtitles.Embedded(int.Parse(args[1])));
var media = new MediaItem(Guid.Parse(args[0]), args[1], new Subtitles.Embedded(int.Parse(args[2])));
var start = TimeSpan.Parse(args[2]);
var duration = TimeSpan.Parse(args[3]);
var start = TimeSpan.Parse(args[3]);
var duration = TimeSpan.Parse(args[4]);
var clipId = Guid.CreateVersion7(utcNow);
var clipPath = Path.Join(args[4], $"{clipId}.mp4");
var description = await clipDescriber.DescribeClip(media, start, duration, lifetime.ApplicationStopping);
await clipExtractor.ExtractClip(media, start, duration, clipPath, lifetime.ApplicationStopping);
logger.LogInformation("Description: {Caption} ({Tags})", description.Caption, description.Tags.Select(t => t.Value));
await app.StopAsync();
src/Cli/appsettings.Development.json
+3
-1
diff --git a/src/Cli/appsettings.Development.json b/src/Cli/appsettings.Development.json
index 4887028..bdeaaee 100644
@@ -12,7 +12,9 @@
"PublishInterval": "00:00:01"
},
"ClipDescriber": {
"Prompt": "Give one short sentence caption and tags for the attached frames from a video clip, optimized for engagement on TikTok and Instagram Reels. Answer with JSON, one string field for `caption` and an array of plain strings for `tags` with at least 2 values. Make the caption one sentence only optimized for engagement, and the tags one word or camel case without the hashtag symbol."
"Prompt": "Give one short sentence caption and tags for the attached frames from a video clip, optimized for engagement on TikTok and Instagram Reels. Answer with JSON, one string field for `caption` and an array of plain strings for `tags` with at least 2 values. Make the caption one sentence only optimized for engagement, and the tags one word or camel case without the hashtag symbol.",
"MovieMetadataPrompt": "The clip comes from {0} released on {1}.",
"ShowMetadataPrompt": "The clip comes from episode {0} of {1} released on {2}."
},
"ClipSelector": {
"ClippableQuotes": [
src/Domain/Describer/ClipDescriber.cs
+28
-2
diff --git a/src/Domain/Describer/ClipDescriber.cs b/src/Domain/Describer/ClipDescriber.cs
index 2c7e6e0..dda25fc 100644
@@ -12,6 +12,7 @@ namespace Slopper.Domain.Describer;
public sealed class ClipDescriber(
IFrameExtractor frameExtractor,
ISubtitleReader subtitleReader,
IMediaRepository mediaRepository,
IChatClient chatClient,
IOptionsMonitor<ClipDescriberOptions> options
)
@@ -23,8 +24,17 @@ public sealed class ClipDescriber(
CancellationToken cancellationToken
)
{
var frames = await frameExtractor.ExtractFrames(media, [start, start + duration], cancellationToken);
var subtitles = await subtitleReader.ReadSubtitles(media, start, duration, cancellationToken);
var framesTask = frameExtractor.ExtractFrames(
media,
[start, start + duration / 3, start + 2 * duration / 3, start + duration],
cancellationToken
);
var subtitlesTask = subtitleReader.ReadSubtitles(media, start, duration, cancellationToken);
var metadataTask = mediaRepository.GetMediaInfo(media.Id, cancellationToken);
var frames = await framesTask;
var subtitles = await subtitlesTask;
var metadata = await metadataTask;
List<AIContent> contents = [];
foreach (var frame in frames)
@@ -32,6 +42,22 @@ public sealed class ClipDescriber(
contents.Add(new DataContent(frame, "image/png"));
}
contents.Add(new TextContent(options.CurrentValue.Prompt));
contents.Add(
new TextContent(
metadata.SeriesName is null
? string.Format(
options.CurrentValue.MovieMetadataPrompt,
metadata.Name,
metadata.OriginalReleaseDate
)
: string.Format(
options.CurrentValue.ShowMetadataPrompt,
metadata.Name,
metadata.SeriesName,
metadata.OriginalReleaseDate
)
)
);
if (subtitles.Count > 0)
{
contents.Add(
src/Domain/Describer/ClipDescriberOptions.cs
+6
-0
diff --git a/src/Domain/Describer/ClipDescriberOptions.cs b/src/Domain/Describer/ClipDescriberOptions.cs
index 37529ee..8cec0cc 100644
@@ -7,6 +7,12 @@ public sealed class ClipDescriberOptions
{
[Required]
public required string Prompt { get; set; }
[Required]
public required string MovieMetadataPrompt { get; set; }
[Required]
public required string ShowMetadataPrompt { get; set; }
}
[OptionsValidator]