feat: wip...migrating to proper app

This commit is contained in:
Stevan Freeborn
2025-07-23 23:55:56 -05:00
parent 439cd6a310
commit 310f56fb84
20 changed files with 370 additions and 64 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
[*.cs]
dotnet_diagnostic.CA2007.severity = none
dotnet_diagnostic.CA2007.severity = none
@@ -0,0 +1,45 @@
namespace StreamShorts.Console.Commands;
internal class DefaultCommand(
IFileSystem fileSystem,
IAnsiConsole console
) : AsyncCommand<DefaultCommand.Settings>
{
private readonly IFileSystem _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
private readonly IAnsiConsole _console = console ?? throw new ArgumentNullException(nameof(console));
internal class Settings : CommandSettings
{
[CommandArgument(0, "[Stream]")]
[Description("The path to the stream")]
public string Stream { get; init; } = string.Empty;
}
public override ValidationResult Validate(CommandContext context, Settings settings)
{
if (string.IsNullOrWhiteSpace(settings.Stream))
{
return ValidationResult.Error("Stream path must be provided.");
}
if (_fileSystem.File.Exists(settings.Stream) is false)
{
return ValidationResult.Error($"The specified stream file '{settings.Stream}' does not exist.");
}
var fileExtension = _fileSystem.Path.GetExtension(settings.Stream).ToUpperInvariant();
if (fileExtension != ".MP4")
{
return ValidationResult.Error("The specified stream file must be an .mp4 file.");
}
return base.Validate(context, settings);
}
public override Task<int> ExecuteAsync(CommandContext context, Settings settings)
{
_console.Write($"[green]Processing stream:[/] {settings.Stream}");
return Task.FromResult(0);
}
}
@@ -0,0 +1,21 @@
namespace StreamShorts.Console.Hosting;
internal static class HostBuilderExtensions
{
public static CommandApp<DefaultCommand> BuildApp(this IHostBuilder builder)
{
var registrar = new TypeRegistrar(builder);
var app = new CommandApp<DefaultCommand>(registrar);
app.Configure(static c =>
c.SetExceptionHandler(static (ex, resolver) =>
{
var console = resolver?.Resolve(typeof(IAnsiConsole)) as IAnsiConsole;
console?.WriteLine($"[red]An error occurred while executing the command:[/]");
console?.WriteException(ex, ExceptionFormats.ShortenEverything);
})
);
return app;
}
}
@@ -0,0 +1,28 @@
namespace StreamShorts.Console.Hosting;
internal class TypeRegistrar(IHostBuilder builder) : ITypeRegistrar
{
private readonly IHostBuilder _builder = builder;
public ITypeResolver Build()
{
return new TypeResolver(_builder.Build());
}
public void Register(Type service, Type implementation)
{
_builder.ConfigureServices((_, services) => services.AddSingleton(service, implementation));
}
public void RegisterInstance(Type service, object implementation)
{
_builder.ConfigureServices((_, services) => services.AddSingleton(service, implementation));
}
public void RegisterLazy(Type service, Func<object> func)
{
ArgumentNullException.ThrowIfNull(func);
_builder.ConfigureServices((_, services) => services.AddSingleton(service, _ => func()));
}
}
@@ -0,0 +1,16 @@
namespace StreamShorts.Console.Hosting;
internal class TypeResolver(IHost provider) : ITypeResolver, IDisposable
{
private readonly IHost _host = provider ?? throw new ArgumentNullException(nameof(provider));
public object? Resolve(Type? type)
{
return type is not null ? _host.Services.GetService(type) : null;
}
public void Dispose()
{
_host.Dispose();
}
}
+35 -29
View File
@@ -1,5 +1,4 @@
using System.Diagnostics;
using System.Globalization;
using System.Globalization;
using System.Resources;
using System.Text;
using System.Text.Json;
@@ -9,28 +8,47 @@ using FFMpegCore;
using FFMpegCore.Enums;
using FFMpegCore.Pipes;
using Microsoft.Extensions.Configuration;
using NAudio.Wave;
using Whisper.net;
using Whisper.net.Ggml;
var config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.Build();
Log.Logger = new LoggerConfiguration()
.WriteTo.File(
formatter: new CompactJsonFormatter(),
path: Path.Combine(AppContext.BaseDirectory, "logs", "log.jsonl"),
rollingInterval: RollingInterval.Day
)
.Enrich.FromLogContext()
.MinimumLevel.Verbose()
.MinimumLevel.Override("Microsoft", LogEventLevel.Fatal)
.CreateLogger();
var stopwatch = new Stopwatch();
stopwatch.Start();
var resourceManager = new ResourceManager("StreamShorts.Console.Resources.Resources", typeof(Program).Assembly);
Console.WriteLine(resourceManager.GetString("WelcomeMessage", CultureInfo.CurrentCulture));
// Step 1: Retrieve stream
if (args.Length is 0)
try
{
Console.WriteLine(resourceManager.GetString("StreamNotProvided", CultureInfo.CurrentCulture));
return;
var appName = Assembly.GetExecutingAssembly().GetName().Name;
Log.Information("Starting {AppName}", appName);
await Host.CreateDefaultBuilder(args)
.ConfigureLogging(static l => l.ClearProviders())
.ConfigureServices(static (_, services) =>
{
services.AddSingleton(AnsiConsole.Console);
services.AddSingleton<IFileSystem, FileSystem>();
})
.BuildApp()
.RunAsync(args);
Log.Information("{AppName} has completed successfully.", appName);
}
catch (Exception ex)
{
Log.Fatal(ex, "An unhandled exception occurred during execution.");
throw;
}
finally
{
await Log.CloseAndFlushAsync();
}
using var mp3Stream = new MemoryStream();
@@ -44,12 +62,6 @@ var wasExtracted = await FFMpegArguments
)
.ProcessAsynchronously();
if (wasExtracted is false)
{
Console.WriteLine(resourceManager.GetString("Mp3ExtractionFailed", CultureInfo.CurrentCulture));
return;
}
// Step 2: Convert MP3 stream to 16khz wave format
mp3Stream.Position = 0;
using var reader = new Mp3FileReader(mp3Stream);
@@ -141,8 +153,6 @@ Here is the transcript of my YouTube live stream:
{{completeTranscription}}
""";
var apiKey = config["GeminiApiKey"];
if (string.IsNullOrWhiteSpace(apiKey))
{
throw new InvalidOperationException("GeminiApiKey is not configured in appsettings.json.");
@@ -210,10 +220,6 @@ foreach (var result in analysis)
);
}
stopwatch.Stop();
Console.WriteLine(stopwatch.Elapsed.Minutes);
// Step 6: Use analysis to generate a short video
record LLMAnalysis(
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<data name="WelcomeMessage" xml:space="preserve">
<value>Welcome to StreamShorts!</value>
</data>
<data name="StreamNotProvided" xml:space="preserve">
<value>No stream provided. Please specify a stream to process.</value>
</data>
<data name="Mp3ExtractionFailed" xml:space="preserve">
<value>Failed to extract MP3 from the provided stream.</value>
</data>
</root>
@@ -8,7 +8,15 @@
<PackageReference Include="FFMpegCore" />
<PackageReference Include="Microsoft.Extensions.Configuration" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="NAudio" />
<PackageReference Include="Serilog" />
<PackageReference Include="Serilog.Extensions.Hosting" />
<PackageReference Include="Serilog.Formatting.Compact" />
<PackageReference Include="Serilog.Sinks.File" />
<PackageReference Include="Spectre.Console" />
<PackageReference Include="Spectre.Console.Cli" />
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" />
<PackageReference Include="Whisper.net.AllRuntimes" />
</ItemGroup>
+18
View File
@@ -0,0 +1,18 @@
global using System.ComponentModel;
global using System.IO.Abstractions;
global using System.Reflection;
global using Microsoft.Extensions.Configuration;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.Hosting;
global using Microsoft.Extensions.Logging;
global using Serilog;
global using Serilog.Events;
global using Serilog.Formatting.Compact;
global using Spectre.Console;
global using Spectre.Console.Cli;
global using StreamShorts.Console.Commands;
global using StreamShorts.Console.Hosting;