chore: initial commit
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user