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 AnalyzeAsync(DietImageAnalysisJob job, CancellationToken ct) { var imageUrls = new List(); 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); } }