Files
Foxel/Services/AI/AiService.cs

449 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Net.Http.Headers;
using System.Text.Json.Serialization;
using Foxel.Services.Configuration;
using Foxel.Utils;
namespace Foxel.Services.AI;
public class AiService : IAiService
{
private readonly HttpClient _httpClient;
private readonly IConfigService _configService;
public AiService(HttpClient httpClient, IConfigService configService)
{
_httpClient = httpClient;
_configService = configService;
}
private void ConfigureHttpClient()
{
string apiKey = _configService["AI:ApiKey"];
string baseUrl = _configService["AI:ApiEndpoint"];
if (_httpClient.BaseAddress?.ToString() != baseUrl)
{
_httpClient.BaseAddress = new Uri(baseUrl);
}
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
}
public async Task<(string title, string description)> AnalyzeImageAsync(string base64Image)
{
try
{
ConfigureHttpClient();
string model = _configService["AI:Model"];
var imageUrl = new ImageUrl
{
Url = $"data:image/jpeg;base64,{base64Image}"
};
var imageContent = new ImageUrlContent
{
Type = "image_url",
ImageUrl = imageUrl
};
var textContent = new TextContent
{
Type = "text",
Text =
"请详细分析这张图片,并提供全面的描述,以便用于向量嵌入和基于文本的图像搜索。描述需要包含:主体对象、场景环境、色彩特点、构图布局、风格特征、情绪氛围、细节特征等关键元素。请提供一个简短有力的标题,然后提供详细描述。\n\n请以JSON格式返回格式如下\n{\"title\": \"简短概括图片的核心内容\", \"description\": \"全面详细的描述,包含上述所有元素,使用丰富精确的词汇,避免笼统表达\"}\n\n请确保返回有效的JSON格式。"
};
var message = new ChatMessage
{
Role = "user",
Content = new MessageContent[] { imageContent, textContent }
};
var requestContent = new ChatCompletionRequest
{
Model = model,
Messages = [message],
Stream = false,
MaxTokens = 800,
Temperature = 0.5,
TopP = 0.8,
TopK = 50
};
var response = await _httpClient.PostAsJsonAsync("/v1/chat/completions", requestContent);
response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadFromJsonAsync<AiResponse>();
if (responseContent?.Choices == null || responseContent.Choices.Length == 0)
{
return ("未能获取标题", "未能获取描述");
}
var aiMessage = responseContent.Choices[0].Message.Content;
return AiHelper.ExtractTitleAndDescription(aiMessage);
}
catch (Exception ex)
{
Console.WriteLine($"AI分析图片时出错: {ex.Message}");
return ("处理失败", $"AI分析过程中发生错误: {ex.Message}");
}
}
public async Task<List<string>> MatchTagsAsync(string description, List<string> availableTags)
{
try
{
// 确保使用最新配置
ConfigureHttpClient();
if (availableTags.Count == 0)
return new List<string>();
string model = _configService["AI:Model"];
var tagsText = string.Join(", ", availableTags);
var textContent = new TextContent
{
Type = "text",
Text =
$"以下是一组标签:[{tagsText}]。\n\n请从这些标签中严格选择与下面描述内容高度相关的标签最多选择5个。只选择确实匹配的标签如果找不到完全匹配或高度相关的标签宁可返回空数组也不要选择不太相关的标签。\n\n描述内容{description}\n\n请以JSON格式返回格式如下\n{{\"tags\": [\"标签1\", \"标签2\", \"标签3\"]}}\n\n请确保返回有效的JSON格式前面不要加```,并且只包含确实匹配的标签名称。"
};
var message = new ChatMessage
{
Role = "user",
Content = new MessageContent[] { textContent }
};
var requestContent = new ChatCompletionRequest
{
Model = model,
Messages = [message],
Stream = false,
MaxTokens = 200,
Temperature = 0.1, // 降低温度使结果更确定性
TopP = 0.95,
TopK = 50
};
var response = await _httpClient.PostAsJsonAsync("/v1/chat/completions", requestContent);
response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadFromJsonAsync<AiResponse>();
if (responseContent?.Choices == null || responseContent.Choices.Length == 0)
{
return new List<string>();
}
var aiMessage = responseContent.Choices[0].Message.Content;
if (string.IsNullOrEmpty(aiMessage))
return new List<string>();
if (aiMessage.Contains("{") && aiMessage.Contains("}"))
{
try
{
int jsonStartIndex = aiMessage.IndexOf('{');
int jsonEndIndex = aiMessage.LastIndexOf('}') + 1;
if (jsonStartIndex >= 0 && jsonEndIndex > jsonStartIndex)
{
string jsonPart = aiMessage[jsonStartIndex..jsonEndIndex];
var options = new System.Text.Json.JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
var result =
System.Text.Json.JsonSerializer.Deserialize<AiHelper.TagsResult>(jsonPart, options);
if (result is { Tags.Length: > 0 })
{
// 确保返回的标签真的在可用标签列表中
var matchedTags = new List<string>();
foreach (var tagName in result.Tags)
{
if (string.IsNullOrWhiteSpace(tagName))
continue;
// 找到大小写完全匹配的标签
var exactMatch = availableTags.FirstOrDefault(t =>
string.Equals(t, tagName, StringComparison.OrdinalIgnoreCase));
if (exactMatch != null)
{
matchedTags.Add(exactMatch);
}
}
return matchedTags.Distinct().ToList();
}
}
}
catch (System.Text.Json.JsonException)
{
// JSON解析失败返回空列表
return new List<string>();
}
}
// 解析失败或没有找到匹配标签,返回空列表
return new List<string>();
}
catch (Exception ex)
{
Console.WriteLine($"AI匹配标签时出错: {ex.Message}");
return new List<string>();
}
}
public async Task<List<string>> GenerateTagsFromImageAsync(string base64Image, List<string> availableTags,
bool allowNewTags = false)
{
try
{
// 确保使用最新配置
ConfigureHttpClient();
string model = _configService["AI:Model"];
var imageUrl = new ImageUrl
{
Url = $"data:image/jpeg;base64,{base64Image}"
};
var imageContent = new ImageUrlContent
{
Type = "image_url",
ImageUrl = imageUrl
};
string promptText;
if (allowNewTags)
{
// 如果允许新标签,则提供现有标签作为参考,但允许生成新标签
promptText = availableTags.Count > 0
? $"可以参考这些现有标签:[{string.Join(", ", availableTags)}],但也可以生成其他与图片内容相关的新标签。\n\n请为图片生成5个最相关的标签优先使用已有标签但如果有更恰当的新标签也可以使用。\n\n请以JSON格式返回格式如下\n{{\"tags\": [\"标签1\", \"标签2\", \"标签3\", \"标签4\", \"标签5\"]}}\n\n请确保返回有效的JSON格式。"
: "请为图片生成5个最相关的标签每个标签应该是简短且描述性的词语或短语。\n\n请以JSON格式返回格式如下\n{\"tags\": [\"标签1\", \"标签2\", \"标签3\", \"标签4\", \"标签5\"]}\n\n请确保返回有效的JSON格式。";
}
else
{
// 如果不允许新标签,则只能从已有标签中选择
if (availableTags.Count == 0)
return new List<string>();
var tagsText = string.Join(", ", availableTags);
promptText =
$"以下是一组标签:[{tagsText}]。\n\n请从这些标签中严格选择与图片内容高度相关的标签最多选择5个。只选择确实匹配的标签如果找不到完全匹配或高度相关的标签宁可返回空数组也不要选择不太相关的标签。\n\n请以JSON格式返回格式如下\n{{\"tags\": [\"标签1\", \"标签2\", \"标签3\"]}}\n\n请确保返回有效的JSON格式并且只包含上述列表中的标签名称。";
}
var textContent = new TextContent
{
Type = "text",
Text = promptText
};
var message = new ChatMessage
{
Role = "user",
Content = new MessageContent[] { imageContent, textContent }
};
var requestContent = new ChatCompletionRequest
{
Model = model,
Messages = [message],
Stream = false,
MaxTokens = 200,
Temperature = 0.1, // 降低温度使结果更确定性
TopP = 0.95,
TopK = 50
};
var response = await _httpClient.PostAsJsonAsync("/v1/chat/completions", requestContent);
response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadFromJsonAsync<AiResponse>();
if (responseContent?.Choices == null || responseContent.Choices.Length == 0)
{
return new List<string>();
}
var aiMessage = responseContent.Choices[0].Message.Content;
if (string.IsNullOrEmpty(aiMessage))
return new List<string>();
if (aiMessage.Contains("{") && aiMessage.Contains("}"))
{
try
{
int jsonStartIndex = aiMessage.IndexOf('{');
int jsonEndIndex = aiMessage.LastIndexOf('}') + 1;
if (jsonStartIndex >= 0 && jsonEndIndex > jsonStartIndex)
{
string jsonPart = aiMessage[jsonStartIndex..jsonEndIndex];
var options = new System.Text.Json.JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
var result =
System.Text.Json.JsonSerializer.Deserialize<AiHelper.TagsResult>(jsonPart, options);
if (result is { Tags.Length: > 0 })
{
var matchedTags = new List<string>();
foreach (var tagName in result.Tags)
{
if (string.IsNullOrWhiteSpace(tagName))
continue;
// 如果允许新标签,直接添加
if (allowNewTags)
{
matchedTags.Add(tagName.Trim());
}
else
{
// 否则只添加已有标签列表中的标签
var exactMatch = availableTags.FirstOrDefault(t =>
string.Equals(t, tagName, StringComparison.OrdinalIgnoreCase));
if (exactMatch != null)
{
matchedTags.Add(exactMatch);
}
}
}
return matchedTags.Distinct().ToList();
}
}
}
catch (System.Text.Json.JsonException)
{
// JSON解析失败返回空列表
return new List<string>();
}
}
// 解析失败或没有找到匹配标签,返回空列表
return new List<string>();
}
catch (Exception ex)
{
Console.WriteLine($"AI从图片生成标签时出错: {ex.Message}");
return new List<string>();
}
}
public async Task<float[]> GetEmbeddingAsync(string text)
{
try
{
// 确保使用最新配置
ConfigureHttpClient();
string model = _configService["AI:EmbeddingModel"];
var requestContent = new
{
model,
input = text,
encoding_format = "float"
};
var response = await _httpClient.PostAsJsonAsync("/v1/embeddings", requestContent);
response.EnsureSuccessStatusCode();
var embedResult = await response.Content.ReadFromJsonAsync<EmbeddingResponse>();
if (embedResult?.Data == null || embedResult.Data.Length == 0)
{
Console.WriteLine("嵌入向量API返回空结果");
return Array.Empty<float>();
}
return embedResult.Data[0].Embedding;
}
catch (Exception ex)
{
Console.WriteLine($"获取嵌入向量时出错: {ex.Message}");
return Array.Empty<float>();
}
}
// 从EmbeddingService移植的私有记录类
private record EmbeddingResponse
{
[JsonPropertyName("data")] public EmbeddingData[] Data { get; set; } = Array.Empty<EmbeddingData>();
}
private record EmbeddingData
{
[JsonPropertyName("embedding")] public float[] Embedding { get; set; } = Array.Empty<float>();
}
private class AiResponse
{
[JsonPropertyName("choices")] public Choice[] Choices { get; set; } = Array.Empty<Choice>();
}
private class Choice
{
[JsonPropertyName("message")] public Message Message { get; set; } = new Message();
}
private class Message
{
[JsonPropertyName("content")] public string Content { get; set; } = string.Empty;
}
}
public class ChatCompletionRequest
{
[JsonPropertyName("model")] public string Model { get; set; } = string.Empty;
[JsonPropertyName("messages")] public ChatMessage[] Messages { get; set; } = Array.Empty<ChatMessage>();
[JsonPropertyName("stream")] public bool Stream { get; set; }
[JsonPropertyName("max_tokens")] public int MaxTokens { get; set; }
[JsonPropertyName("temperature")] public double Temperature { get; set; }
[JsonPropertyName("top_p")] public double TopP { get; set; }
[JsonPropertyName("top_k")] public int TopK { get; set; }
}
public class ChatMessage
{
[JsonPropertyName("role")] public string Role { get; set; } = string.Empty;
[JsonPropertyName("content")] public MessageContent[] Content { get; set; } = Array.Empty<MessageContent>();
}
[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")]
[JsonDerivedType(typeof(TextContent), typeDiscriminator: "text")]
[JsonDerivedType(typeof(ImageUrlContent), typeDiscriminator: "image_url")]
public abstract class MessageContent
{
[JsonPropertyName("type")] public string Type { get; set; } = string.Empty;
}
public class TextContent : MessageContent
{
[JsonPropertyName("text")] public string Text { get; set; } = string.Empty;
}
public class ImageUrlContent : MessageContent
{
[JsonPropertyName("image_url")] public ImageUrl ImageUrl { get; set; } = new();
}
public class ImageUrl
{
[JsonPropertyName("url")] public string Url { get; set; } = string.Empty;
}