- 后端: 新增 AttachmentContextBuilder 解析图片/PDF 摘要并拼入 LLM 上下文; ai_chat_endpoints 扩展附件接口; 新增 ReportAnalysisService - 前端: 新增历史会话页与 conversation_history_provider; chat 链路支持附件展示与回放 - UI: 重构 medication_checkin / notification_center / profile / health_drawer 等多页面 - 配置: api_client baseUrl 适配当前 WiFi IP
72 lines
3.7 KiB
C#
72 lines
3.7 KiB
C#
using Health.Application.Diets;
|
||
using Health.Infrastructure.AI;
|
||
|
||
namespace Health.Infrastructure.Diets;
|
||
|
||
public sealed class DietImageAnalyzer(VisionClient vision) : IDietImageAnalyzer
|
||
{
|
||
private readonly VisionClient _vision = vision;
|
||
|
||
public async Task<DietImageAnalysisResult> AnalyzeAsync(DietImageAnalysisJob job, CancellationToken ct)
|
||
{
|
||
var imageUrls = new List<string>();
|
||
foreach (var filePath in job.FilePaths)
|
||
{
|
||
if (!File.Exists(filePath))
|
||
return new DietImageAnalysisResult(false, 40004, null, "待识别图片不存在");
|
||
|
||
var bytes = await File.ReadAllBytesAsync(filePath, ct);
|
||
var base64 = Convert.ToBase64String(bytes);
|
||
if (base64.Length > 7 * 1024 * 1024)
|
||
return new DietImageAnalysisResult(false, 40001, null, "图片过大,请压缩后重新上传");
|
||
|
||
var extension = Path.GetExtension(filePath).ToLowerInvariant();
|
||
var mime = extension switch
|
||
{
|
||
".png" => "image/png",
|
||
".heic" => "image/heic",
|
||
_ => "image/jpeg"
|
||
};
|
||
imageUrls.Add($"data:{mime};base64,{base64}");
|
||
}
|
||
|
||
var prompt = """
|
||
你是营养师助手。请识别图片中所有食物和饮品,估算每项的具体克数或毫升数,输出 JSON 数组。
|
||
|
||
## 无食物判定(必读)
|
||
如果图片中完全没有可食用的食物或饮品(例如是风景、人物、宠物、文档、屏幕截图、纯背景、空盘子等),**直接输出空数组 []**,不要硬编造食物。
|
||
|
||
## 输出格式
|
||
[{"name":"食物名","portion":"具体克数","calories":数字}]
|
||
|
||
## 份量估算依据(容器与参照物对照表)
|
||
- 标准饭碗(口径11cm):满碗米饭≈200g、满碗面条≈250g、满碗粥≈250g、半碗≈100g
|
||
- 标准汤碗(口径14cm):满碗汤≈400ml、半碗≈200ml
|
||
- 标准盘子(口径23cm):满盘菜≈300g、半盘≈150g
|
||
- 普通玻璃杯:满杯水/饮料≈250ml、纸杯≈200ml
|
||
- 鸡蛋1个≈50g、馒头1个≈100g、包子1个≈80g、饺子1个≈15g
|
||
- 香蕉1根≈100g、苹果1个≈200g、橘子1个≈120g
|
||
- 肉块巴掌大小≈80-120g、鸡腿1个≈100g、鱼1条(中等)≈300g
|
||
|
||
## 输出示例(务必模仿此风格)
|
||
正确:{"name":"米饭","portion":"约200g","calories":230}
|
||
正确:{"name":"红烧肉","portion":"约150g","calories":420}
|
||
正确:{"name":"豆浆","portion":"约250ml","calories":80}
|
||
错误:{"name":"米饭","portion":"一碗","calories":230} ← portion 缺克数
|
||
错误:{"name":"汤","portion":"一份","calories":80} ← portion 缺毫升数
|
||
错误:{"name":"水果","portion":"少许","calories":50} ← portion 是模糊词
|
||
|
||
## 硬性要求
|
||
1. 图片若无食物,输出 [] 即可。不要硬凑食物。
|
||
2. portion 字段必须包含阿拉伯数字 + 单位(g 或 ml)。不允许任何只有量词没有克数的回答。
|
||
3. 即使图片不够清楚,也要根据可见容器/参照物给出估算,可加"约"或区间("约80-120g")。
|
||
4. calories 是数字(千卡),不带单位、不带引号。
|
||
|
||
只输出 JSON 数组本身,不要任何前缀、后缀、代码块标记或说明文字。
|
||
""";
|
||
var response = await _vision.VisionAsync(prompt, imageUrls, userText: null, maxTokens: 8192, ct: ct);
|
||
var result = response.Choices?.FirstOrDefault()?.Message?.Content ?? "[]";
|
||
return new DietImageAnalysisResult(true, 0, result, null);
|
||
}
|
||
}
|