feat: complete implementation using gemini as analyzer

This commit is contained in:
Stevan Freeborn
2025-08-19 12:43:34 -05:00
parent 85304eadd1
commit d470574b99
12 changed files with 135 additions and 143 deletions
+11 -13
View File
@@ -1,4 +1,3 @@
using FFMpegCore;
using FFMpegCore.Enums;
using FFMpegCore.Pipes;
@@ -23,19 +22,18 @@ internal sealed class FFMpegService : IVideoService
.ConfigureAwait(false);
}
public async Task<Stream> ExtractClipFromVideoAsync(Stream video, TimeSpan start, TimeSpan end, TimeSpan? buffer = null)
public async Task CreateClipFromVideoAsync(
string sourcePath,
string destinationPath,
TimeSpan startTime,
TimeSpan endTime,
TimeSpan? buffer = null
)
{
var startTime = start - (buffer ?? TimeSpan.Zero);
var endTime = end + (buffer ?? TimeSpan.Zero);
var outputStream = new MemoryStream();
await FFMpegArguments
.FromPipeInput(new StreamPipeSource(video), o => o.Seek(startTime).EndSeek(endTime))
.OutputToPipe(new StreamPipeSink(outputStream), o => o.CopyChannel().ForceFormat("webm"))
.ProcessAsynchronously()
var start = startTime - (buffer ?? TimeSpan.Zero);
var end = endTime + (buffer ?? TimeSpan.Zero);
await FFMpeg.SubVideoAsync(sourcePath, destinationPath, start, end)
.ConfigureAwait(false);
outputStream.Position = 0;
return outputStream;
}
}
@@ -12,14 +12,21 @@ internal interface IVideoService
/// <param name="audio">The output audio stream.</param>
/// <returns>A task that represents the asynchronous operation. The task result indicates whether the extraction was successful.</returns>
Task<bool> ExtractAudioFromVideoAsync(Stream video, Stream audio);
/// <summary>
/// Extracts a clip from a video stream.
/// Creates a clip from a video file based on the specified start and end times.
/// </summary>
/// <param name="video">The input video stream.</param>
/// <param name="start">The start time of the clip.</param>
/// <param name="end">The end time of the clip.</param>
/// <param name="buffer">The duration of the buffer to include before the start time and after the end time.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the extracted clip as a stream.</returns>
Task<Stream> ExtractClipFromVideoAsync(Stream video, TimeSpan start, TimeSpan end, TimeSpan? buffer = null);
/// <param name="sourcePath">The path to the source video file.</param>
/// <param name="destinationPath">The path where the created clip will be saved.</param>
/// <param name="startTime">The start time of the clip.</param>
/// <param name="endTime">The end time of the clip.</param>
/// <param name="buffer">An optional buffer time to include before and after the clip segment.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task CreateClipFromVideoAsync(
string sourcePath,
string destinationPath,
TimeSpan startTime,
TimeSpan endTime,
TimeSpan? buffer = null
);
}
@@ -8,13 +8,14 @@ namespace StreamShorts.Library.Media.Video;
public interface IShortsCreator
{
/// <summary>
/// Creates video shorts from the provided transcript analysis and video stream.
/// Creates a video short from the specified source video file based on the provided candidate details.
/// </summary>
/// <param name="analysis">The transcript analysis.</param>
/// <param name="video">The video stream.</param>
/// <param name="buffer">An optional buffer duration to include before the start time and after the end time of each short.</param>
/// <returns>An asynchronous enumerable of <see cref="ShortClip"/>.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="analysis"/> is null.</exception>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="video"/> is null.</exception>
public IAsyncEnumerable<ShortClip> CreateShortsAsync(TranscriptAnalysis analysis, Stream video, TimeSpan? buffer = null);
/// <param name="sourcePath">The path to the source video file.</param>
/// <param name="candidate">The details of the short candidate.</param>
/// <param name="destinationPath">The path where the created short will be saved.</param>
/// <param name="buffer">An optional buffer time to include before and after the short segment.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="sourcePath"/> or <paramref name="destinationPath"/> is null or whitespace.</exception>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="candidate"/> is null.</exception>
public Task CreateShortAsync(string sourcePath, ShortCandidate candidate, string destinationPath, TimeSpan? buffer = null);
}
@@ -1,11 +0,0 @@
using StreamShorts.Library.Analysis;
namespace StreamShorts.Library.Media.Video;
/// <summary>
/// Represents a short video clip.
/// </summary>
public record ShortClip(
ShortCandidate Candidate,
Stream Segment
);
@@ -19,27 +19,24 @@ public class ShortsCreator : IShortsCreator
_videoService = videoService ?? throw new ArgumentNullException(nameof(videoService), $"{nameof(videoService)} cannot be null");
}
public async IAsyncEnumerable<ShortClip> CreateShortsAsync(TranscriptAnalysis analysis, Stream video, TimeSpan? buffer = null)
public async Task CreateShortAsync(string sourcePath, ShortCandidate candidate, string destinationPath, TimeSpan? buffer = null)
{
if (analysis is null)
if (string.IsNullOrWhiteSpace(sourcePath))
{
throw new ArgumentNullException(nameof(analysis), $"{nameof(analysis)} cannot be null");
throw new ArgumentNullException(nameof(sourcePath), $"{nameof(sourcePath)} cannot be null or whitespace");
}
if (video is null)
if (candidate is null)
{
throw new ArgumentNullException(nameof(video), $"{nameof(video)} cannot be null");
throw new ArgumentNullException(nameof(candidate), $"{nameof(candidate)} cannot be null");
}
foreach (var candidate in analysis.Candidates)
if (string.IsNullOrWhiteSpace(destinationPath))
{
var clip = await _videoService.ExtractClipFromVideoAsync(video, candidate.StartTime, candidate.EndTime, buffer)
.ConfigureAwait(false);
yield return new ShortClip(
candidate,
clip
);
throw new ArgumentNullException(nameof(destinationPath), $"{nameof(destinationPath)} cannot be null or whitespace");
}
await _videoService.CreateClipFromVideoAsync(sourcePath, destinationPath, candidate.StartTime, candidate.EndTime, buffer)
.ConfigureAwait(false);
}
}