chore: initial commit

This commit is contained in:
Stevan Freeborn
2025-02-04 14:39:54 -06:00
commit a1a682916e
45 changed files with 1736 additions and 0 deletions
@@ -0,0 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="FluentAssertions" Version="7.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="RichardSzalay.MockHttp" Version="7.0.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<!--TODO: Add test coverage-->
<ItemGroup>
<Using Include="Xunit" />
<Using Include="FluentAssertions" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AltGen.API\AltGen.API.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="Files\**\*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="appsettings.Test.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>
@@ -0,0 +1,7 @@
namespace AltGen.API.Tests.EndToEnd;
public class EndToEndTest(AppFactory factory, TestConfiguration config) : IClassFixture<AppFactory>, IClassFixture<TestConfiguration>
{
protected HttpClient Client { get; } = factory.CreateClient();
protected TestConfiguration Config { get; } = config;
}
@@ -0,0 +1,31 @@
namespace AltGen.API.Tests.EndToEnd;
public class GenerateEndpointTests(
AppFactory factory,
TestConfiguration config
) : EndToEndTest(factory, config)
{
[Fact]
public async Task GenerateEndpoint_WhenCalled_ItShouldReturnOk()
{
var fileName = "library.jpg";
var file = await TestFileManager.GetFileAsync(fileName);
var byteContent = new ByteArrayContent(file);
byteContent.Headers.ContentType = new MediaTypeHeaderValue("image/jpg");
var content = new MultipartFormDataContent()
{
{ new StringContent("Gemini"), "Provider" },
{ new StringContent(Config.GeminiApiKey), "ProviderKey" },
{ byteContent, "File", fileName }
};
var response = await Client.PostAsync("/generate", content);
var altText = await response.Content.ReadFromJsonAsync<GenerateResponse>();
response.Should().HaveStatusCode(HttpStatusCode.OK);
altText!.AltText.Should().NotBeNullOrWhiteSpace();
}
}
@@ -0,0 +1,49 @@
namespace AltGen.API.Tests.EndToEnd;
public class PromptEvaluations(
AppFactory factory,
TestConfiguration config
) : EndToEndTest(factory, config)
{
// TODO: Use LLM-assisted prompt evaluation instead of
// doing partial string matching. Better suited due to
// subjective nature of the task.
[Theory]
[ClassData(typeof(TestData))]
public async Task Generate_WhenCalled_ItShouldRespondWithAltTextContainingKeyWords(string imageName, string[] keyWords)
{
var file = await TestFileManager.GetFileAsync(imageName);
var byteContent = new ByteArrayContent(file);
byteContent.Headers.ContentType = new MediaTypeHeaderValue("image/jpg");
var content = new MultipartFormDataContent()
{
{ new StringContent("Gemini"), "Provider" },
{ new StringContent(Config.GeminiApiKey), "ProviderKey" },
{ byteContent, "File", imageName }
};
var response = await Client.PostAsync("/generate", content);
var altText = await response.Content.ReadFromJsonAsync<GenerateResponse>();
response.Should().HaveStatusCode(HttpStatusCode.OK);
altText!.AltText.ToLowerInvariant().Should().ContainAll(keyWords);
}
class TestData : IEnumerable<object[]>
{
public IEnumerator<object[]> GetEnumerator()
{
yield return new object[] { "library.jpg", new[] { "book", } };
yield return new object[] { "ascii_art.jpg", new[] { "test", "100", "success", } };
yield return new object[] { "living_room.jpg", new[] { "blue", "dog", } };
yield return new object[] { "podcast.jpg", new[] { "podcast", "scott", "mark", } };
yield return new object[] { "youtube_comment.jpg", new[] { "feedback", } };
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

@@ -0,0 +1,11 @@
using Microsoft.Extensions.Logging;
namespace AltGen.API.Tests.Fixtures;
public class AppFactory : WebApplicationFactory<Program>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureLogging(static l => l.ClearProviders());
}
}
@@ -0,0 +1,27 @@
using Microsoft.Extensions.Configuration;
namespace AltGen.API.Tests.Fixtures;
public class TestConfiguration
{
static IConfiguration Config { get; } = new ConfigurationBuilder()
.AddJsonFile("appsettings.Test.json")
.AddEnvironmentVariables()
.Build();
#pragma warning disable CA1822
public string GeminiApiKey => GetGeminiApiKey();
#pragma warning restore CA1822
static string GetGeminiApiKey()
{
var apiKey = Config.GetSection("Gemini").GetValue<string>("ApiKey");
if (string.IsNullOrWhiteSpace(apiKey))
{
throw new InvalidOperationException("Gemini__ApiKey is required");
}
return apiKey;
}
}
@@ -0,0 +1,211 @@
namespace AltGen.API.Tests.Integration;
public class GenerateEndpointTests : IntegrationTest
{
const string TestImageName = "library.jpg";
public GenerateEndpointTests(AppFactory factory) : base(factory)
{
MockGeminiServiceHandler.Clear();
}
[Theory]
[ClassData(typeof(InvalidRequestTestData))]
public async Task GenerateEndpoint_WhenCalledWithoutInvalidParameters_ItShouldReturnAProblemDetailWithStatusCode400(MultipartFormDataContent content, Dictionary<string, string[]> errors)
{
var response = await Client.PostAsync("/generate", content);
var problem = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>();
response.Should().HaveStatusCode(HttpStatusCode.BadRequest);
problem!.Errors.Should().BeEquivalentTo(errors);
}
[Theory]
[ClassData(typeof(ValidRequestTestData))]
public async Task GenerateEndpoint_WhenCalledWithRequiredParameters_ItShouldReturnOkWithAltText(MultipartFormDataContent content)
{
MockGeminiServiceHandler
.When(HttpMethod.Post, "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent/?key=ProviderKey")
.Respond(
"application/json",
/*lang=json,strict*/
@"{
""candidates"": [
{
""content"": {
""parts"": [
{ ""text"": ""A long, perspective view of a classic library interior showcases richly colored wooden bookshelves filled with antique books, creating an atmosphere of history and scholarship.\n""
}
],
""role"": ""model""
},
""finishReason"": ""STOP"",
""avgLogprobs"": -0.3317602475484212
}
],
""usageMetadata"": {
""promptTokenCount"": 405,
""candidatesTokenCount"": 30,
""totalTokenCount"": 435,
""promptTokensDetails"": [
{
""modality"": ""TEXT"",
""tokenCount"": 147
},
{
""modality"": ""IMAGE"",
""tokenCount"": 258
}
],
""candidatesTokensDetails"": [
{
""modality"": ""TEXT"",
""tokenCount"": 30
}
]
},
""modelVersion"": ""gemini-1.5-flash""
}"
);
var response = await Client.PostAsync("/generate", content);
var altText = await response.Content.ReadFromJsonAsync<GenerateResponse>();
response.Should().HaveStatusCode(HttpStatusCode.OK);
altText!.AltText.Should().NotBeNullOrWhiteSpace();
}
class ValidRequestTestData : IEnumerable<object[]>
{
public IEnumerator<object[]> GetEnumerator()
{
var testImage = TestFileManager.GetFile(TestImageName);
var byteContent = new ByteArrayContent(testImage);
byteContent.Headers.ContentType = new MediaTypeHeaderValue("image/jpg");
yield return new object[]
{
new MultipartFormDataContent()
{
{ new StringContent("Gemini"), "Provider" },
{ new StringContent("ProviderKey"), "ProviderKey" },
{ byteContent, "File", TestImageName }
}
};
yield return new object[]
{
new MultipartFormDataContent()
{
{ new StringContent("Gemini"), "Provider" },
{ new StringContent("ProviderKey"), "ProviderKey" },
{ byteContent, "File", TestImageName }
}
};
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
class InvalidRequestTestData : IEnumerable<object[]>
{
public IEnumerator<object[]> GetEnumerator()
{
yield return new object[]
{
new MultipartFormDataContent()
{
{ new StringContent(string.Empty), "Provider" },
{ new StringContent(string.Empty), "ProviderKey" },
{ new ByteArrayContent([]), "File", "file.jpeg" }
},
new Dictionary<string, string[]>
{
{ "Provider", ["The Provider field is required."] },
{ "ProviderKey", ["The ProviderKey field is required."] },
{ "File", ["The File has no content."] }
},
};
yield return new object[]
{
new MultipartFormDataContent()
{
{ new StringContent("Gemini"), "Provider" },
{ new StringContent(string.Empty), "ProviderKey" },
{ new ByteArrayContent([]), "File", "file.jpeg" }
},
new Dictionary<string, string[]>
{
{ "ProviderKey", ["The ProviderKey field is required."] },
{ "File", ["The File has no content."] }
},
};
yield return new object[]
{
new MultipartFormDataContent()
{
{ new StringContent(string.Empty), "Provider" },
{ new StringContent("ProviderKey"), "ProviderKey" },
{ new ByteArrayContent([]), "File", "file.jpeg" }
},
new Dictionary<string, string[]>
{
{ "Provider", ["The Provider field is required."] },
{ "File", ["The File has no content."] }
},
};
yield return new object[]
{
new MultipartFormDataContent()
{
{ new StringContent("Gemini"), "Provider" },
{ new StringContent("ProviderKey"), "ProviderKey" },
{ new ByteArrayContent([]), "File", "file.jpeg" }
},
new Dictionary<string, string[]>
{
{ "File", ["The File has no content."] }
},
};
yield return new object[]
{
new MultipartFormDataContent()
{
{ new StringContent("MadeUp"), "Provider" },
{ new StringContent("ProviderKey"), "ProviderKey" },
{ new ByteArrayContent(Encoding.UTF8.GetBytes("Hello, World!")), "File", "file.jpeg" }
},
new Dictionary<string, string[]>()
{
{ "Provider", ["The Provider field is invalid."] }
},
};
yield return new object[]
{
new MultipartFormDataContent()
{
{ new StringContent("Gemini"), "Provider" },
{ new StringContent("ProviderKey"), "ProviderKey" },
{ new ByteArrayContent(Encoding.UTF8.GetBytes("Hello, World!")), "File", "file.txt" }
},
new Dictionary<string, string[]>()
{
{ "File", ["The File must be a valid image file."] }
},
};
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}
@@ -0,0 +1,20 @@
namespace AltGen.API.Tests.Integration;
public class IntegrationTest : IClassFixture<AppFactory>
{
protected MockHttpMessageHandler MockGeminiServiceHandler { get; } = new();
protected WebApplicationFactory<Program> Factory { get; }
protected HttpClient Client { get; }
public IntegrationTest(AppFactory factory)
{
Factory = factory.WithWebHostBuilder(
builder => builder.ConfigureTestServices(
services => services.AddHttpClient<IGeminiService, GeminiService>()
.ConfigurePrimaryHttpMessageHandler(() => MockGeminiServiceHandler)
)
);
Client = Factory.CreateClient();
}
}
+18
View File
@@ -0,0 +1,18 @@
global using System.Collections;
global using System.Net;
global using System.Net.Http.Headers;
global using System.Net.Http.Json;
global using System.Text;
global using AltGen.API.Generate;
global using AltGen.API.Generate.Providers.Gemini;
global using AltGen.API.Tests.Fixtures;
global using AltGen.API.Tests.Utils;
global using Microsoft.AspNetCore.Hosting;
global using Microsoft.AspNetCore.Mvc;
global using Microsoft.AspNetCore.Mvc.Testing;
global using Microsoft.AspNetCore.TestHost;
global using Microsoft.Extensions.DependencyInjection;
global using RichardSzalay.MockHttp;
@@ -0,0 +1,18 @@
namespace AltGen.API.Tests.Utils;
public static class TestFileManager
{
public static byte[] GetFile(string testFileName)
{
var filePath = Path.Combine(AppContext.BaseDirectory, "Files", testFileName);
var file = File.ReadAllBytes(filePath);
return file;
}
public static Task<byte[]> GetFileAsync(string testFileName)
{
var filePath = Path.Combine(AppContext.BaseDirectory, "Files", testFileName);
var file = File.ReadAllBytesAsync(filePath);
return file;
}
}
@@ -0,0 +1,5 @@
{
"Gemini": {
"ApiKey": "ApiKey"
}
}
+20
View File
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.1" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="$(AssemblyName).Tests" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\**\*" />
</ItemGroup>
</Project>
+6
View File
@@ -0,0 +1,6 @@
@AltGen.API_HostAddress = http://localhost:5005
GET {{AltGen.API_HostAddress}}/weatherforecast/
Accept: application/json
###
+5
View File
@@ -0,0 +1,5 @@
namespace AltGen.API.Common;
class AltGenException(string message) : Exception(message)
{
}
@@ -0,0 +1,14 @@
namespace AltGen.API.Common;
static class ValidationResultsExtensions
{
public static Dictionary<string, string[]> ToErrors(this IEnumerable<ValidationResult> results)
{
// group by the first member name and select the error message
return results.GroupBy(static r => r.MemberNames.First())
.ToDictionary(
static r => r.Key,
static r => r.Select(static e => e.ErrorMessage!).ToArray()
);
}
}
@@ -0,0 +1,27 @@
namespace AltGen.API.Generate;
static class GenerateEndpoint
{
public static async Task<IResult> HandleAsync([AsParameters] GenerateRequest request, [FromServices] IAltTextProviderFactory factory)
{
var validationResults = request.Validate(new ValidationContext(request));
if (validationResults.Any())
{
return Results.ValidationProblem(validationResults.ToErrors());
}
var provider = factory.Create(request.Provider);
var imageStream = new MemoryStream();
await request.File.CopyToAsync(imageStream);
var altText = await provider.GenerateAltTextAsync(
request.ProviderKey,
request.File.ContentType,
imageStream
);
return Results.Ok(new GenerateResponse(altText));
}
}
@@ -0,0 +1,47 @@
namespace AltGen.API.Generate;
record GenerateRequest(
[FromForm]
string Provider,
[FromForm]
string ProviderKey,
IFormFile File
) : IValidatableObject
{
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (string.IsNullOrWhiteSpace(Provider))
{
yield return new ValidationResult($"The {nameof(Provider)} field is required.", [nameof(Provider)]);
}
if (string.IsNullOrWhiteSpace(Provider) is false && LLMProvider.IsValid(Provider) is false)
{
yield return new ValidationResult($"The {nameof(Provider)} field is invalid.", [nameof(Provider)]);
}
if (string.IsNullOrWhiteSpace(ProviderKey))
{
yield return new ValidationResult($"The {nameof(ProviderKey)} field is required.", [nameof(ProviderKey)]);
}
// TODO: Probably need to consider upper bound for file size
// this also needs to take into account provider-specific limits
if (File.Length is 0)
{
yield return new ValidationResult($"The {nameof(File)} has no content.", [nameof(File)]);
}
if (File.Length > 0)
{
var extension = Path.GetExtension(File.FileName);
// TODO: prob needs to be specific to the provider
// different LLMs support different image formats
if (extension is not ".jpeg" and not ".jpg" and not ".png")
{
yield return new ValidationResult($"The {nameof(File)} must be a valid image file.", [nameof(File)]);
}
}
}
}
@@ -0,0 +1,3 @@
namespace AltGen.API.Generate;
record GenerateResponse(string AltText);
@@ -0,0 +1,15 @@
namespace AltGen.API.Generate.Providers;
class AltTextProviderFactory(IServiceProvider serviceProvider) : IAltTextProviderFactory
{
readonly IServiceProvider _serviceProvider = serviceProvider;
public IAltTextProvider Create(string provider)
{
return provider switch
{
LLMProvider.Gemini => _serviceProvider.GetRequiredKeyedService<IAltTextProvider>(LLMProvider.Gemini),
_ => throw new NotSupportedException($"The provider '{provider}' is not supported.")
};
}
}
@@ -0,0 +1,3 @@
namespace AltGen.API.Generate.Providers.Gemini;
record Candidate(Content Content);
@@ -0,0 +1,3 @@
namespace AltGen.API.Generate.Providers.Gemini;
record Content(Part[] Parts, string Role);
@@ -0,0 +1,11 @@
namespace AltGen.API.Generate.Providers.Gemini;
class GeminiAltTextProvider(IGeminiService geminiService) : IAltTextProvider
{
readonly IGeminiService _geminiService = geminiService;
public Task<string> GenerateAltTextAsync(string providerKey, string mimeType, MemoryStream image)
{
return _geminiService.GenerateContentAsync(providerKey, mimeType, image);
}
}
@@ -0,0 +1,6 @@
namespace AltGen.API.Generate.Providers.Gemini;
record GeminiRequest(
Content SystemInstruction,
Content[] Contents
);
@@ -0,0 +1,3 @@
namespace AltGen.API.Generate.Providers.Gemini;
record GeminiResponse(Candidate[] Candidates);
@@ -0,0 +1,8 @@
namespace AltGen.API.Generate.Providers.Gemini;
static class GeminiRole
{
public const string System = "system";
public const string User = "user";
public const string Model = "model";
}
@@ -0,0 +1,79 @@
namespace AltGen.API.Generate.Providers.Gemini;
class GeminiService(HttpClient httpClient) : IGeminiService
{
const string BaseUri = "https://generativelanguage.googleapis.com/v1beta/models";
const string Method = "generateContent";
const string ModelId = "gemini-1.5-flash";
const string ApiKeyQueryKey = "key";
// TODO: Use Lazy<T> to load the prompt resource
string _prompt = "";
string Prompt
{
get
{
if (string.IsNullOrWhiteSpace(_prompt))
{
var resource = Assembly.GetExecutingAssembly()
.GetManifestResourceStream("AltGen.API.Resources.prompt.txt") ?? throw new AltGenException("Failed to load the prompt resource.");
using var reader = new StreamReader(resource);
_prompt = reader.ReadToEnd();
}
return _prompt;
}
}
static readonly JsonSerializerOptions Options = new()
{
PropertyNameCaseInsensitive = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters = { new PartConverter() }
};
readonly HttpClient _httpClient = httpClient;
public async Task<string> GenerateContentAsync(string providerKey, string mimeType, MemoryStream image)
{
var requestUri = GetRequestUri(providerKey);
var imageBase64 = ConvertToBase64(image);
var request = new GeminiRequest(
new Content([new TextPart(Prompt)], GeminiRole.System),
[new Content(
[
new TextPart(""),
new InlineDataPart(new InlineData(mimeType, imageBase64))
],
GeminiRole.User
)]
);
var response = await _httpClient.PostAsJsonAsync(requestUri, request, Options);
var content = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode is false)
{
throw new AltGenException("Failed to generate alt text.");
}
var geminiResponse = JsonSerializer.Deserialize<GeminiResponse>(content, Options) ?? throw new AltGenException("Failed to deserialize the Gemini response.");
var firstCandidate = geminiResponse.Candidates.FirstOrDefault() ?? throw new AltGenException("No candidates found in the Gemini response.");
var altText = firstCandidate.Content.Parts
.OfType<TextPart>()
.Aggregate(new StringBuilder(), static (sb, part) => sb.Append(part.Text))
.ToString();
return altText;
}
static string GetRequestUri(string providerKey)
{
return $"{BaseUri}/{ModelId}:{Method}/?{ApiKeyQueryKey}={providerKey}";
}
static string ConvertToBase64(MemoryStream image)
{
return Convert.ToBase64String(image.ToArray());
}
}
@@ -0,0 +1,6 @@
namespace AltGen.API.Generate.Providers.Gemini;
interface IGeminiService
{
Task<string> GenerateContentAsync(string providerKey, string mimeType, MemoryStream image);
}
@@ -0,0 +1,9 @@
namespace AltGen.API.Generate.Providers.Gemini;
record Part();
record TextPart(string Text) : Part;
record InlineDataPart(InlineData InlineData) : Part;
record InlineData(string MimeType, string Data);
@@ -0,0 +1,27 @@
namespace AltGen.API.Generate.Providers.Gemini;
class PartConverter : JsonConverter<Part>
{
public override Part? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
using var doc = JsonDocument.ParseValue(ref reader);
var root = doc.RootElement;
if (root.TryGetProperty("text", out var text))
{
return JsonSerializer.Deserialize<TextPart>(root.GetRawText(), options);
}
if (root.TryGetProperty("inlineData", out var inlineData))
{
return JsonSerializer.Deserialize<InlineDataPart>(root.GetRawText(), options);
}
throw new JsonException("Invalid part type.");
}
public override void Write(Utf8JsonWriter writer, Part value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value, value.GetType(), options);
}
}
@@ -0,0 +1,6 @@
namespace AltGen.API.Generate.Providers;
interface IAltTextProvider
{
Task<string> GenerateAltTextAsync(string providerKey, string mimeType, MemoryStream image);
}
@@ -0,0 +1,6 @@
namespace AltGen.API.Generate.Providers;
interface IAltTextProviderFactory
{
IAltTextProvider Create(string provider);
}
@@ -0,0 +1,11 @@
namespace AltGen.API.Generate.Providers;
static class LLMProvider
{
public const string Gemini = "Gemini";
public static bool IsValid(string provider)
{
return provider is Gemini;
}
}
+26
View File
@@ -0,0 +1,26 @@
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();
builder.Services.AddOpenApi();
builder.Services.AddHttpClient<IGeminiService, GeminiService>();
builder.Services.AddSingleton<IAltTextProviderFactory, AltTextProviderFactory>();
builder.Services.AddKeyedSingleton<IAltTextProvider, GeminiAltTextProvider>(LLMProvider.Gemini);
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.UseStatusCodePages();
app.MapPost("/generate", GenerateEndpoint.HandleAsync).DisableAntiforgery();
app.Run();
[ExcludeFromCodeCoverage]
public partial class Program { }
@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7297;http://localhost:5005",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5005",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
}
}
+17
View File
@@ -0,0 +1,17 @@
Generate descriptive alt text that captures the essential visual information of the image. Follow these guidelines:
1. Be concise, but comprehensive (1-2 sentences)
2. Describe the most important visual elements
3. Convey the image's purpose or key message
4. Use objective language
5. Avoid redundant phrases
Priority details to include:
- Main subject(s)
- Action or context
- Color or distinctive visual characteristics
- Emotional tone or artistic intent
Exclude unnecessary details like background minutiae or decorative elements unless they are crucial to understanding the image.
Please DO NOT respond with anything over than the alt text.
+13
View File
@@ -0,0 +1,13 @@
global using System.ComponentModel.DataAnnotations;
global using System.Diagnostics.CodeAnalysis;
global using System.Reflection;
global using System.Text;
global using System.Text.Json;
global using System.Text.Json.Serialization;
global using AltGen.API.Common;
global using AltGen.API.Generate;
global using AltGen.API.Generate.Providers;
global using AltGen.API.Generate.Providers.Gemini;
global using Microsoft.AspNetCore.Mvc;
+28
View File
@@ -0,0 +1,28 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AltGen.API", "AltGen.API\AltGen.API.csproj", "{2865924B-D42C-4126-856F-AD0B762710FB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AltGen.API.Tests", "AltGen.API.Tests\AltGen.API.Tests.csproj", "{FC95942B-3579-4F83-A447-A96D8EEF8E45}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{2865924B-D42C-4126-856F-AD0B762710FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2865924B-D42C-4126-856F-AD0B762710FB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2865924B-D42C-4126-856F-AD0B762710FB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2865924B-D42C-4126-856F-AD0B762710FB}.Release|Any CPU.Build.0 = Release|Any CPU
{FC95942B-3579-4F83-A447-A96D8EEF8E45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FC95942B-3579-4F83-A447-A96D8EEF8E45}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FC95942B-3579-4F83-A447-A96D8EEF8E45}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FC95942B-3579-4F83-A447-A96D8EEF8E45}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal