chore: lots of chores

This commit is contained in:
Stevan Freeborn
2025-08-19 16:25:15 -05:00
parent 3752e9dfa0
commit bd0a7fe3cd
23 changed files with 97 additions and 33 deletions
+4
View File
@@ -5,6 +5,10 @@ on:
paths: paths:
- src/StreamShorts.Console/** - src/StreamShorts.Console/**
- src/StreamShorts.Console.Tests/** - src/StreamShorts.Console.Tests/**
- src/StreamShorts.Library/**
- src/StreamShorts.Library.Tests/**
- StreamShorts.sln
- Directory.Build.props
branches: branches:
- main - main
jobs: jobs:
@@ -1,9 +1,8 @@
using System.Text.Json;
using StreamShorts.Library.Media.Video;
namespace StreamShorts.Console.Commands; namespace StreamShorts.Console.Commands;
/// <summary>
/// The default command for processing video streams to create short clips.
/// </summary>
internal sealed class DefaultCommand( internal sealed class DefaultCommand(
IFileSystem fileSystem, IFileSystem fileSystem,
IAnsiConsole console, IAnsiConsole console,
@@ -28,8 +27,14 @@ internal sealed class DefaultCommand(
private readonly IShortsCreator _shortsCreator = shortsCreator ?? throw new ArgumentNullException(nameof(shortsCreator)); private readonly IShortsCreator _shortsCreator = shortsCreator ?? throw new ArgumentNullException(nameof(shortsCreator));
private readonly TimeProvider _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); private readonly TimeProvider _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider));
/// <summary>
/// Represents the settings for the default command.
/// </summary>
internal class Settings : CommandSettings internal class Settings : CommandSettings
{ {
/// <summary>
/// Gets or sets the path to the stream.
/// </summary>
[CommandArgument(0, "[Stream]")] [CommandArgument(0, "[Stream]")]
[Description("The path to the stream")] [Description("The path to the stream")]
public string Stream { get; init; } = string.Empty; public string Stream { get; init; } = string.Empty;
@@ -103,7 +108,7 @@ internal sealed class DefaultCommand(
); );
_fileSystem.Directory.CreateDirectory(outputDirectoryPath); _fileSystem.Directory.CreateDirectory(outputDirectoryPath);
await _fileSystem.File.WriteAllTextAsync( await _fileSystem.File.WriteAllTextAsync(
_fileSystem.Path.Combine(outputDirectoryPath, "transcription.txt"), _fileSystem.Path.Combine(outputDirectoryPath, "transcription.txt"),
string.Join(Environment.NewLine, transcriptionSegments) string.Join(Environment.NewLine, transcriptionSegments)
@@ -1,7 +1,15 @@
namespace StreamShorts.Console.Hosting; namespace StreamShorts.Console.Hosting;
/// <summary>
/// Provides extension methods for building command applications from host builders.
/// </summary>
internal static class HostBuilderExtensions internal static class HostBuilderExtensions
{ {
/// <summary>
/// Builds a command application from the host builder.
/// </summary>
/// <param name="builder">The host builder.</param>
/// <returns>A configured command application.</returns>
public static CommandApp<DefaultCommand> BuildApp(this IHostBuilder builder) public static CommandApp<DefaultCommand> BuildApp(this IHostBuilder builder)
{ {
var registrar = new TypeRegistrar(builder); var registrar = new TypeRegistrar(builder);
@@ -1,5 +1,9 @@
namespace StreamShorts.Console.Hosting; namespace StreamShorts.Console.Hosting;
/// <summary>
/// Provides type registration services for the dependency injection container.
/// </summary>
/// <inheritdoc/>
internal sealed class TypeRegistrar(IHostBuilder builder) : ITypeRegistrar internal sealed class TypeRegistrar(IHostBuilder builder) : ITypeRegistrar
{ {
private readonly IHostBuilder _builder = builder; private readonly IHostBuilder _builder = builder;
@@ -1,5 +1,9 @@
namespace StreamShorts.Console.Hosting; namespace StreamShorts.Console.Hosting;
/// <summary>
/// Provides type resolution services using the dependency injection container.
/// </summary>
/// <inheritdoc/>
internal sealed class TypeResolver(IHost provider) : ITypeResolver, IDisposable internal sealed class TypeResolver(IHost provider) : ITypeResolver, IDisposable
{ {
private readonly IHost _host = provider ?? throw new ArgumentNullException(nameof(provider)); private readonly IHost _host = provider ?? throw new ArgumentNullException(nameof(provider));
+3 -7
View File
@@ -1,8 +1,4 @@
using Microsoft.Extensions.Configuration; Log.Logger = new LoggerConfiguration()
using StreamShorts.Library.Media.Video;
Log.Logger = new LoggerConfiguration()
.WriteTo.File( .WriteTo.File(
formatter: new CompactJsonFormatter(), formatter: new CompactJsonFormatter(),
path: Path.Combine(AppContext.BaseDirectory, "logs", "log.jsonl"), path: Path.Combine(AppContext.BaseDirectory, "logs", "log.jsonl"),
@@ -42,7 +38,7 @@ try
{ {
throw new InvalidOperationException($"{keyOptionName} is not configured in appsettings.json."); throw new InvalidOperationException($"{keyOptionName} is not configured in appsettings.json.");
} }
var clientFactory = sp.GetRequiredService<IHttpClientFactory>(); var clientFactory = sp.GetRequiredService<IHttpClientFactory>();
return new GeminiAnalyzer(clientFactory, key, model); return new GeminiAnalyzer(clientFactory, key, model);
@@ -62,4 +58,4 @@ catch (Exception ex)
finally finally
{ {
await Log.CloseAndFlushAsync(); await Log.CloseAndFlushAsync();
} }
@@ -1,7 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<AssemblyTitle>StreamShorts.Console</AssemblyTitle>
<Product>StreamShorts.Console</Product>
<Description>A command-line interface for StreamShorts</Description>
<Version>0.0.0</Version>
<Authors>Stevan Freeborn</Authors>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
+4 -1
View File
@@ -1,7 +1,9 @@
global using System.ComponentModel; global using System.ComponentModel;
global using System.IO.Abstractions; global using System.IO.Abstractions;
global using System.Reflection; global using System.Reflection;
global using System.Text.Json;
global using Microsoft.Extensions.Configuration;
global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.Hosting; global using Microsoft.Extensions.Hosting;
global using Microsoft.Extensions.Logging; global using Microsoft.Extensions.Logging;
@@ -18,4 +20,5 @@ global using StreamShorts.Console.Hosting;
global using StreamShorts.Library.Analysis; global using StreamShorts.Library.Analysis;
global using StreamShorts.Library.Analysis.Gemini; global using StreamShorts.Library.Analysis.Gemini;
global using StreamShorts.Library.Media.Audio; global using StreamShorts.Library.Media.Audio;
global using StreamShorts.Library.Transcription; global using StreamShorts.Library.Media.Video;
global using StreamShorts.Library.Transcription;
@@ -28,4 +28,4 @@ public sealed class FailedTranscriptAnalysisException : Exception
public FailedTranscriptAnalysisException(string message, Exception innerException) : base(message, innerException) public FailedTranscriptAnalysisException(string message, Exception innerException) : base(message, innerException)
{ {
} }
} }
@@ -1,5 +1,10 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
/// <summary>
/// Represents content with role and parts for Gemini API.
/// </summary>
/// <param name="Role">The role of the content (e.g., "user", "assistant").</param>
/// <param name="Parts">The array of content parts.</param>
internal record Content( internal record Content(
[property: JsonPropertyName("role")] [property: JsonPropertyName("role")]
string Role, string Role,
@@ -7,6 +12,10 @@ internal record Content(
Part[] Parts Part[] Parts
); );
/// <summary>
/// Represents a part of content for Gemini API.
/// </summary>
/// <param name="Text">The text content of the part.</param>
internal record Part( internal record Part(
[property: JsonPropertyName("text")] [property: JsonPropertyName("text")]
string Text string Text
@@ -2,6 +2,11 @@ using System.Text.Json.Serialization;
namespace StreamShorts.Library.Analysis.Gemini; namespace StreamShorts.Library.Analysis.Gemini;
/// <summary>
/// Represents a request to generate content using Gemini.
/// </summary>
/// <param name="Contents">The content array for the request.</param>
/// <param name="GenerationConfig">The generation configuration.</param>
internal record GenerateContentRequest( internal record GenerateContentRequest(
[property: JsonPropertyName("contents")] [property: JsonPropertyName("contents")]
Content[] Contents, Content[] Contents,
@@ -9,6 +14,10 @@ internal record GenerateContentRequest(
GenerationConfig GenerationConfig GenerationConfig GenerationConfig
); );
/// <summary>
/// Represents configuration for content generation.
/// </summary>
/// <param name="ResponseMimeType">The MIME type for the response.</param>
internal record GenerationConfig( internal record GenerationConfig(
[property: JsonPropertyName("responseMimeType")] [property: JsonPropertyName("responseMimeType")]
string ResponseMimeType string ResponseMimeType
@@ -2,13 +2,20 @@ using System.Text.Json.Serialization;
namespace StreamShorts.Library.Analysis.Gemini; namespace StreamShorts.Library.Analysis.Gemini;
/// <summary>
/// Represents a response from the Gemini content generation API.
/// </summary>
/// <param name="Candidates">The array of candidate responses.</param>
internal record GenerateContentResponse( internal record GenerateContentResponse(
[property: JsonPropertyName("candidates")] [property: JsonPropertyName("candidates")]
Candidate[] Candidates Candidate[] Candidates
); );
/// <summary>
/// Represents a candidate response from content generation.
/// </summary>
/// <param name="Content">The generated content.</param>
internal record Candidate( internal record Candidate(
[property: JsonPropertyName("content")] [property: JsonPropertyName("content")]
Content Content Content Content
); );
@@ -16,4 +16,4 @@ public record ShortCandidate(
TimeSpan StartTime, TimeSpan StartTime,
[property: JsonPropertyName("end_time")] [property: JsonPropertyName("end_time")]
TimeSpan EndTime TimeSpan EndTime
); );
@@ -16,10 +16,10 @@ public sealed class AudioExtractor : IAudioExtractor
} }
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AudioExtractor"/> class with a specified FFMpeg service. /// Initializes a new instance of the <see cref="AudioExtractor"/> class with a specified <see cref="IVideoService"/>.
/// </summary> /// </summary>
/// <param name="videoService">The video service to use for audio extraction.</param> /// <param name="videoService">The video service to use for audio extraction.</param>
/// <exception cref="ArgumentNullException">Thrown when the FFMpeg service is null.</exception> /// <exception cref="ArgumentNullException">Thrown when the video service is null.</exception>
internal AudioExtractor(IVideoService videoService) internal AudioExtractor(IVideoService videoService)
{ {
_videoService = videoService ?? throw new ArgumentNullException(nameof(videoService), $"{nameof(videoService)} cannot be null"); _videoService = videoService ?? throw new ArgumentNullException(nameof(videoService), $"{nameof(videoService)} cannot be null");
@@ -23,8 +23,8 @@ internal sealed class FFMpegService : IVideoService
} }
public async Task CreateClipFromVideoAsync( public async Task CreateClipFromVideoAsync(
string sourcePath, string sourcePath,
string destinationPath, string destinationPath,
TimeSpan startTime, TimeSpan startTime,
TimeSpan endTime, TimeSpan endTime,
TimeSpan? buffer = null TimeSpan? buffer = null
@@ -32,7 +32,7 @@ internal sealed class FFMpegService : IVideoService
{ {
var start = startTime - (buffer ?? TimeSpan.Zero); var start = startTime - (buffer ?? TimeSpan.Zero);
var end = endTime + (buffer ?? TimeSpan.Zero); var end = endTime + (buffer ?? TimeSpan.Zero);
await FFMpeg.SubVideoAsync(sourcePath, destinationPath, start, end) await FFMpeg.SubVideoAsync(sourcePath, destinationPath, start, end)
.ConfigureAwait(false); .ConfigureAwait(false);
} }
@@ -12,7 +12,7 @@ internal interface IVideoService
/// <param name="audio">The output audio stream.</param> /// <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> /// <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); Task<bool> ExtractAudioFromVideoAsync(Stream video, Stream audio);
/// <summary> /// <summary>
/// Creates a clip from a video file based on the specified start and end times. /// Creates a clip from a video file based on the specified start and end times.
/// </summary> /// </summary>
@@ -6,14 +6,22 @@ namespace StreamShorts.Library.Media.Video;
/// Represents a service that creates video shorts. /// Represents a service that creates video shorts.
/// </summary> /// </summary>
/// <inheritdoc/> /// <inheritdoc/>
public class ShortsCreator : IShortsCreator public sealed class ShortsCreator : IShortsCreator
{ {
private readonly IVideoService _videoService = new FFMpegService(); private readonly IVideoService _videoService = new FFMpegService();
/// <summary>
/// Initializes a new instance of the <see cref="ShortsCreator"/> class.
/// </summary>
public ShortsCreator() public ShortsCreator()
{ {
} }
/// <summary>
/// Initializes a new instance of the <see cref="ShortsCreator"/> class with a specified <see cref="IVideoService"/>.
/// </summary>
/// <param name="videoService">The video service.</param>
/// <exception cref="ArgumentNullException">Thrown when the video service is null.</exception>
internal ShortsCreator(IVideoService videoService) internal ShortsCreator(IVideoService videoService)
{ {
_videoService = videoService ?? throw new ArgumentNullException(nameof(videoService), $"{nameof(videoService)} cannot be null"); _videoService = videoService ?? throw new ArgumentNullException(nameof(videoService), $"{nameof(videoService)} cannot be null");
@@ -25,17 +33,17 @@ public class ShortsCreator : IShortsCreator
{ {
throw new ArgumentNullException(nameof(sourcePath), $"{nameof(sourcePath)} cannot be null or whitespace"); throw new ArgumentNullException(nameof(sourcePath), $"{nameof(sourcePath)} cannot be null or whitespace");
} }
if (candidate is null) if (candidate is null)
{ {
throw new ArgumentNullException(nameof(candidate), $"{nameof(candidate)} cannot be null"); throw new ArgumentNullException(nameof(candidate), $"{nameof(candidate)} cannot be null");
} }
if (string.IsNullOrWhiteSpace(destinationPath)) if (string.IsNullOrWhiteSpace(destinationPath))
{ {
throw new ArgumentNullException(nameof(destinationPath), $"{nameof(destinationPath)} cannot be null or whitespace"); throw new ArgumentNullException(nameof(destinationPath), $"{nameof(destinationPath)} cannot be null or whitespace");
} }
await _videoService.CreateClipFromVideoAsync(sourcePath, destinationPath, candidate.StartTime, candidate.EndTime, buffer) await _videoService.CreateClipFromVideoAsync(sourcePath, destinationPath, candidate.StartTime, candidate.EndTime, buffer)
.ConfigureAwait(false); .ConfigureAwait(false);
} }
@@ -12,4 +12,4 @@ public interface ITranscriber
/// <param name="cancellationToken">A cancellation token to cancel the operation.</param> /// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
/// <returns>An <see cref="IAsyncEnumerable{T}"/> where T is <see cref="TranscriptionSegment"/>.</returns> /// <returns>An <see cref="IAsyncEnumerable{T}"/> where T is <see cref="TranscriptionSegment"/>.</returns>
IAsyncEnumerable<TranscriptionSegment> TranscribeAsync(Stream audio, CancellationToken cancellationToken = default); IAsyncEnumerable<TranscriptionSegment> TranscribeAsync(Stream audio, CancellationToken cancellationToken = default);
} }
@@ -1,6 +1,6 @@
namespace StreamShorts.Console.Tests.Unit.Hosting; namespace StreamShorts.Console.Tests.Unit.Hosting;
public class TypeRegistrarTests internal class TypeRegistrarTests
{ {
[Fact] [Fact]
public void Constructor_WhenCalled_ItShouldNotThrowShould() public void Constructor_WhenCalled_ItShouldNotThrowShould()
@@ -1,6 +1,6 @@
namespace StreamShorts.Console.Tests.Unit.Hosting; namespace StreamShorts.Console.Tests.Unit.Hosting;
public class TypeResolverTests internal class TypeResolverTests
{ {
[Fact] [Fact]
public void Constructor_WhenCalledWithNullHost_ItShouldThrowArgumentNullException() public void Constructor_WhenCalledWithNullHost_ItShouldThrowArgumentNullException()
+1 -1
View File
@@ -5,4 +5,4 @@ global using Microsoft.Extensions.Hosting;
global using Moq; global using Moq;
global using StreamShorts.Console.Hosting; global using StreamShorts.Console.Hosting;
@@ -1,6 +1,6 @@
namespace StreamShorts.Library.Tests.Integration.Media.Audio; namespace StreamShorts.Library.Tests.Integration.Media.Audio;
public class AudioExtractorTests internal class AudioExtractorTests
{ {
private readonly AudioExtractor _sut = new(); private readonly AudioExtractor _sut = new();
@@ -4,7 +4,7 @@ using StreamShorts.Library.Media;
namespace StreamShorts.Library.Tests.Unit.Media.Audio; namespace StreamShorts.Library.Tests.Unit.Media.Audio;
public class AudioExtractorTests internal class AudioExtractorTests
{ {
private readonly Mock<IVideoService> _mockFfmpegService = new(); private readonly Mock<IVideoService> _mockFfmpegService = new();
private readonly AudioExtractor _sut; private readonly AudioExtractor _sut;