feat: implement generate command in console app
This commit is contained in:
@@ -12,6 +12,8 @@
|
|||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
<PackageReference Include="xunit" Version="2.9.2" />
|
<PackageReference Include="xunit" Version="2.9.2" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
<PackageReference Include="RichardSzalay.MockHttp" Version="7.0.0" />
|
||||||
|
<PackageReference Include="FluentAssertions" Version="7.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
using System.Net;
|
||||||
|
|
||||||
|
using FluentAssertions;
|
||||||
|
namespace AltGen.Console.Tests.Unit;
|
||||||
|
|
||||||
|
public class AltGenServiceTests : IDisposable
|
||||||
|
{
|
||||||
|
readonly MockHttpMessageHandler _mockHttpMessageHandler = new();
|
||||||
|
readonly AltGenService _altGenService;
|
||||||
|
|
||||||
|
public AltGenServiceTests()
|
||||||
|
{
|
||||||
|
_altGenService = new AltGenService(_mockHttpMessageHandler.ToHttpClient());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GenerateAltTextAsync_WhenCalledAndRequestSucceeds_ItShouldReturnAltText()
|
||||||
|
{
|
||||||
|
var req = new GenerateAltTextRequest(
|
||||||
|
"provider",
|
||||||
|
"key",
|
||||||
|
"file.jpg",
|
||||||
|
[1, 2, 3],
|
||||||
|
"image/jpeg"
|
||||||
|
);
|
||||||
|
|
||||||
|
var altText = "alt text";
|
||||||
|
|
||||||
|
_mockHttpMessageHandler
|
||||||
|
.When(HttpMethod.Post, "*/generate")
|
||||||
|
.Respond(
|
||||||
|
"application/json",
|
||||||
|
/*lang=json,strict*/
|
||||||
|
$@"{{""AltText"":""{altText}""}}"
|
||||||
|
);
|
||||||
|
|
||||||
|
var result = await _altGenService.GenerateAltTextAsync(req);
|
||||||
|
|
||||||
|
result.Should().Be(altText);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GenerateAltTextAsync_WhenCalledAndRequestFails_ItShouldThrowException()
|
||||||
|
{
|
||||||
|
var req = new GenerateAltTextRequest(
|
||||||
|
"provider",
|
||||||
|
"key",
|
||||||
|
"file.jpg",
|
||||||
|
[1, 2, 3],
|
||||||
|
"image/jpeg"
|
||||||
|
);
|
||||||
|
|
||||||
|
_mockHttpMessageHandler
|
||||||
|
.When(HttpMethod.Post, "*/generate")
|
||||||
|
.Respond(HttpStatusCode.InternalServerError);
|
||||||
|
|
||||||
|
var action = async () => await _altGenService.GenerateAltTextAsync(req);
|
||||||
|
|
||||||
|
await action.Should().ThrowAsync<AltTextException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GenerateAltTextAsync_WhenCalledAndResponseIsNull_ItShouldThrowException()
|
||||||
|
{
|
||||||
|
var req = new GenerateAltTextRequest(
|
||||||
|
"provider",
|
||||||
|
"key",
|
||||||
|
"file.jpg",
|
||||||
|
[1, 2, 3],
|
||||||
|
"image/jpeg"
|
||||||
|
);
|
||||||
|
|
||||||
|
_mockHttpMessageHandler
|
||||||
|
.When(HttpMethod.Post, "*/generate")
|
||||||
|
.Respond("application/json", "null");
|
||||||
|
|
||||||
|
var action = async () => await _altGenService.GenerateAltTextAsync(req);
|
||||||
|
|
||||||
|
await action.Should().ThrowAsync<AltTextException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_mockHttpMessageHandler.Dispose();
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
namespace AltGen.Console.Tests;
|
|
||||||
|
|
||||||
public class UnitTest1
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void Test1()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
global using RichardSzalay.MockHttp;
|
||||||
|
|
||||||
|
global using AltGen.Console.Generate;
|
||||||
@@ -16,4 +16,34 @@
|
|||||||
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="21.3.1" />
|
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="21.3.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="$(AssemblyName).Tests" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<GeneratedCodeDirectory>Generated</GeneratedCodeDirectory>
|
||||||
|
<GeneratedConstantsFile>$(GeneratedCodeDirectory)\Constants.cs</GeneratedConstantsFile>
|
||||||
|
<AltGenApiUri>http://localhost:7297</AltGenApiUri>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<Target Name="GenerateBuildConstants" BeforeTargets="BeforeBuild;BeforeRebuild">
|
||||||
|
|
||||||
|
<MakeDir Directories="$(GeneratedCodeDirectory)" />
|
||||||
|
|
||||||
|
<WriteLinesToFile
|
||||||
|
File="$(GeneratedConstantsFile)" Lines="
|
||||||
|
namespace $(AssemblyName).$(GeneratedCodeDirectory)%3b
|
||||||
|
|
||||||
|
static class Constants
|
||||||
|
{
|
||||||
|
public const string AltGenApiUri = "$(AltGenApiUri)"%3b
|
||||||
|
}
|
||||||
|
"
|
||||||
|
Overwrite="true" />
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="$(GeneratedConstantsFile)" Exclude="@(Compile)" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Target>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
namespace AltGen.Console.Generate;
|
||||||
|
|
||||||
|
sealed class AltGenService(HttpClient client) : IAltGenService
|
||||||
|
{
|
||||||
|
static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
};
|
||||||
|
readonly HttpClient _client = client;
|
||||||
|
|
||||||
|
public async Task<string> GenerateAltTextAsync(GenerateAltTextRequest req)
|
||||||
|
{
|
||||||
|
var byteContent = new ByteArrayContent(req.Image);
|
||||||
|
byteContent.Headers.ContentType = new MediaTypeHeaderValue(req.ContentType);
|
||||||
|
|
||||||
|
var request = new HttpRequestMessage(HttpMethod.Post, $"{Constants.AltGenApiUri}/generate")
|
||||||
|
{
|
||||||
|
Content = new MultipartFormDataContent
|
||||||
|
{
|
||||||
|
{ new StringContent(req.Provider), "provider" },
|
||||||
|
{ new StringContent(req.ProviderKey), "providerKey" },
|
||||||
|
{ new ByteArrayContent(req.Image), "file", req.FileName }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await _client.SendAsync(request);
|
||||||
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode is false)
|
||||||
|
{
|
||||||
|
throw new AltTextException("Failed to generate alt text.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var altTextResponse = JsonSerializer.Deserialize<AltTextResponse>(content, JsonOptions) ?? throw new AltTextException("Failed to deserialize response.");
|
||||||
|
|
||||||
|
return altTextResponse.AltText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record AltTextResponse(string AltText);
|
||||||
|
|
||||||
|
class AltTextException(string message) : Exception(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace AltGen.Console.Generate;
|
||||||
|
|
||||||
|
record GenerateAltTextRequest(
|
||||||
|
string Provider,
|
||||||
|
string ProviderKey,
|
||||||
|
string FileName,
|
||||||
|
byte[] Image,
|
||||||
|
string ContentType
|
||||||
|
);
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
namespace AltGen.Console.Generate;
|
||||||
|
|
||||||
|
sealed class GenerateCommand(
|
||||||
|
IAnsiConsole console,
|
||||||
|
IFileSystem fileSystem,
|
||||||
|
IAltGenService altGenService
|
||||||
|
) : AsyncCommand<GenerateCommand.Settings>
|
||||||
|
{
|
||||||
|
|
||||||
|
readonly IAnsiConsole _console = console;
|
||||||
|
readonly IFileSystem _fileSystem = fileSystem;
|
||||||
|
readonly IAltGenService _altGenService = altGenService;
|
||||||
|
|
||||||
|
public class Settings(IFileSystem fileSystem) : CommandSettings
|
||||||
|
{
|
||||||
|
readonly Dictionary<string, string> _imageTypes = new()
|
||||||
|
{
|
||||||
|
[".jpeg"] = "image/jpeg",
|
||||||
|
[".jpg"] = "image/jpeg",
|
||||||
|
[".png"] = "image/png"
|
||||||
|
};
|
||||||
|
|
||||||
|
readonly List<string> _providers = [
|
||||||
|
"gemini",
|
||||||
|
];
|
||||||
|
|
||||||
|
readonly IFileSystem _fileSystem = fileSystem;
|
||||||
|
|
||||||
|
[CommandArgument(1, "<provider>")]
|
||||||
|
[Description("The provider to use for generating alt text.")]
|
||||||
|
public string Provider { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[CommandArgument(2, "<key>")]
|
||||||
|
[Description("The key for the provider.")]
|
||||||
|
public string Key { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
[CommandArgument(3, "<path>")]
|
||||||
|
[Description("The path to the image to generate alt text for.")]
|
||||||
|
public string Path { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public string ContentType => _imageTypes[_fileSystem.Path.GetExtension(Path)];
|
||||||
|
|
||||||
|
public override ValidationResult Validate()
|
||||||
|
{
|
||||||
|
// TODO: We should allow provider and key to be optional
|
||||||
|
// if they are not passed on the command line then
|
||||||
|
// we should try to resolve them from configuration
|
||||||
|
if (_providers.Contains(Provider) is false)
|
||||||
|
{
|
||||||
|
return ValidationResult.Error($"The provider '{Provider}' is not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var pathExists = _fileSystem.File.Exists(Path);
|
||||||
|
|
||||||
|
if (pathExists is false)
|
||||||
|
{
|
||||||
|
return ValidationResult.Error($"The path '{Path}' does not exist.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var extension = _fileSystem.Path.GetExtension(Path);
|
||||||
|
|
||||||
|
if (_imageTypes.ContainsKey(extension) is false)
|
||||||
|
{
|
||||||
|
return ValidationResult.Error($"The file '{Path}' is not a valid image file.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ValidationResult.Success();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<int> ExecuteAsync(CommandContext context, Settings settings)
|
||||||
|
{
|
||||||
|
var fileName = _fileSystem.Path.GetFileName(settings.Path);
|
||||||
|
var image = await _fileSystem.File.ReadAllBytesAsync(settings.Path);
|
||||||
|
|
||||||
|
var altText = await _altGenService.GenerateAltTextAsync(new(
|
||||||
|
settings.Provider,
|
||||||
|
settings.Key,
|
||||||
|
fileName,
|
||||||
|
image,
|
||||||
|
settings.ContentType
|
||||||
|
));
|
||||||
|
|
||||||
|
_console.MarkupLine($"[bold]{altText}[/]");
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
interface IAltGenService
|
||||||
|
{
|
||||||
|
Task<string> GenerateAltTextAsync(GenerateAltTextRequest req);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace AltGen.Console.Generated;
|
||||||
|
|
||||||
|
static class Constants
|
||||||
|
{
|
||||||
|
public const string AltGenApiUri = "http://localhost:7297";
|
||||||
|
}
|
||||||
+12
-151
@@ -1,18 +1,17 @@
|
|||||||
// TODO: We want to be able
|
// TODO: Need to implement config
|
||||||
// to provide a path to an image
|
// command. This should allow
|
||||||
// read the image
|
// the user to set default values
|
||||||
// and then post the image to our API
|
// for provider keys and a default
|
||||||
// get the response and display the alt text
|
// provider.
|
||||||
|
//
|
||||||
using System.ComponentModel;
|
// For example setting a providers key
|
||||||
using System.IO.Abstractions;
|
// i.e. altgen config gemini mykey
|
||||||
using System.Net.Http.Headers;
|
//
|
||||||
|
// Or setting provider as default
|
||||||
using AltGen.Console.Common;
|
// i.e. altgen config gemini mykey --default
|
||||||
|
|
||||||
using Spectre.Console;
|
|
||||||
|
|
||||||
await Host.CreateDefaultBuilder(args)
|
await Host.CreateDefaultBuilder(args)
|
||||||
|
.ConfigureLogging(static l => l.ClearProviders())
|
||||||
.ConfigureServices(static (_, services) =>
|
.ConfigureServices(static (_, services) =>
|
||||||
{
|
{
|
||||||
services.AddSingleton<IFileSystem, FileSystem>();
|
services.AddSingleton<IFileSystem, FileSystem>();
|
||||||
@@ -22,141 +21,3 @@ await Host.CreateDefaultBuilder(args)
|
|||||||
})
|
})
|
||||||
.BuildApp()
|
.BuildApp()
|
||||||
.RunAsync(args);
|
.RunAsync(args);
|
||||||
|
|
||||||
sealed class GenerateCommand(
|
|
||||||
IAnsiConsole console,
|
|
||||||
IFileSystem fileSystem,
|
|
||||||
IAltGenService altGenService
|
|
||||||
) : AsyncCommand<GenerateCommand.Settings>
|
|
||||||
{
|
|
||||||
|
|
||||||
readonly IAnsiConsole _console = console;
|
|
||||||
readonly IFileSystem _fileSystem = fileSystem;
|
|
||||||
readonly IAltGenService _altGenService = altGenService;
|
|
||||||
|
|
||||||
public class Settings(IFileSystem fileSystem) : CommandSettings
|
|
||||||
{
|
|
||||||
readonly Dictionary<string, string> _imageTypes = new()
|
|
||||||
{
|
|
||||||
[".jpeg"] = "image/jpeg",
|
|
||||||
[".jpg"] = "image/jpeg",
|
|
||||||
[".png"] = "image/png"
|
|
||||||
};
|
|
||||||
|
|
||||||
readonly List<string> _providers = [
|
|
||||||
"gemini",
|
|
||||||
];
|
|
||||||
|
|
||||||
readonly IFileSystem _fileSystem = fileSystem;
|
|
||||||
|
|
||||||
[CommandArgument(1, "<provider>")]
|
|
||||||
[Description("The provider to use for generating alt text.")]
|
|
||||||
public string Provider { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
[CommandArgument(2, "<key>")]
|
|
||||||
[Description("The key for the provider.")]
|
|
||||||
public string Key { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
[CommandArgument(3, "<path>")]
|
|
||||||
[Description("The path to the image to generate alt text for.")]
|
|
||||||
public string Path { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
public string ContentType => _imageTypes[_fileSystem.Path.GetExtension(Path)];
|
|
||||||
|
|
||||||
public override ValidationResult Validate()
|
|
||||||
{
|
|
||||||
if (_providers.Contains(Provider) is false)
|
|
||||||
{
|
|
||||||
return ValidationResult.Error($"The provider '{Provider}' is not supported.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var pathExists = _fileSystem.File.Exists(Path);
|
|
||||||
|
|
||||||
if (pathExists is false)
|
|
||||||
{
|
|
||||||
return ValidationResult.Error($"The path '{Path}' does not exist.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var extension = _fileSystem.Path.GetExtension(Path);
|
|
||||||
|
|
||||||
if (_imageTypes.ContainsKey(extension) is false)
|
|
||||||
{
|
|
||||||
return ValidationResult.Error($"The file '{Path}' is not a valid image file.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return ValidationResult.Success();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async Task<int> ExecuteAsync(CommandContext context, Settings settings)
|
|
||||||
{
|
|
||||||
var fileName = _fileSystem.Path.GetFileName(settings.Path);
|
|
||||||
var image = await _fileSystem.File.ReadAllBytesAsync(settings.Path);
|
|
||||||
|
|
||||||
var altText = await _altGenService.GenerateAltTextAsync(
|
|
||||||
settings.Provider,
|
|
||||||
settings.Key,
|
|
||||||
fileName,
|
|
||||||
image,
|
|
||||||
settings.ContentType
|
|
||||||
);
|
|
||||||
|
|
||||||
_console.MarkupLine($"[bold]Alt Text:[/] {altText}");
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IAltGenService
|
|
||||||
{
|
|
||||||
Task<string> GenerateAltTextAsync(
|
|
||||||
string provider,
|
|
||||||
string key,
|
|
||||||
string fileName,
|
|
||||||
byte[] image,
|
|
||||||
string contentType
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class AltGenService(HttpClient client) : IAltGenService
|
|
||||||
{
|
|
||||||
readonly HttpClient _client = client;
|
|
||||||
|
|
||||||
public async Task<string> GenerateAltTextAsync(
|
|
||||||
string provider,
|
|
||||||
string key,
|
|
||||||
string fileName,
|
|
||||||
byte[] image,
|
|
||||||
string contentType
|
|
||||||
)
|
|
||||||
{
|
|
||||||
var byteContent = new ByteArrayContent(image);
|
|
||||||
byteContent.Headers.ContentType = new MediaTypeHeaderValue(contentType);
|
|
||||||
|
|
||||||
// TODO: Need to have proper URL
|
|
||||||
// have constants generated at build time
|
|
||||||
// that point to proper API URL
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Post, "altgen")
|
|
||||||
{
|
|
||||||
Content = new MultipartFormDataContent
|
|
||||||
{
|
|
||||||
{ new StringContent(provider), "provider" },
|
|
||||||
{ new StringContent(key), "providerKey" },
|
|
||||||
{ new ByteArrayContent(image), "file", fileName }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var response = await _client.SendAsync(request);
|
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode is false)
|
|
||||||
{
|
|
||||||
// TODO: Use accurate exception
|
|
||||||
throw new InvalidDataException("Failed to generate alt text.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Deserialize the response
|
|
||||||
var altText = await response.Content.ReadAsStringAsync();
|
|
||||||
|
|
||||||
return altText;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,15 @@
|
|||||||
|
global using System.ComponentModel;
|
||||||
|
global using System.IO.Abstractions;
|
||||||
|
global using System.Net.Http.Headers;
|
||||||
|
global using System.Text.Json;
|
||||||
|
|
||||||
|
global using AltGen.Console.Common;
|
||||||
|
global using AltGen.Console.Generated;
|
||||||
|
global using AltGen.Console.Generate;
|
||||||
|
|
||||||
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 Spectre.Console;
|
||||||
global using Spectre.Console.Cli;
|
global using Spectre.Console.Cli;
|
||||||
|
|||||||
Reference in New Issue
Block a user