feat(diet-records): 新增营养成分表图片分析功能

- 添加营养成分表图片识别API接口,支持通过AI模型分析食物营养成分
- 新增NutritionAnalysisService服务,集成GLM-4.5V和Qwen VL视觉模型
- 实现营养成分提取和健康建议生成功能
- 添加完整的API文档和TypeScript类型定义
- 支持多种营养素类型识别,包括热量、蛋白质、脂肪等20+种营养素
This commit is contained in:
richarjiang
2025-10-16 10:03:22 +08:00
parent cc83b84c80
commit 5c2c9dfae8
7 changed files with 684 additions and 3 deletions

View File

@@ -0,0 +1,282 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { OpenAI } from 'openai';
/**
* 营养成分分析结果接口
*/
export interface NutritionAnalysisResult {
key: string; // 营养素的唯一标识,如 energy_kcal
name: string; // 营养素的中文名称,如"热量"
value: string; // 从图片中识别的原始值和单位,如"840千焦"
analysis: string; // 针对该营养素的详细健康建议
}
/**
* 营养成分分析响应接口
*/
export interface NutritionAnalysisResponse {
success: boolean;
data: NutritionAnalysisResult[];
message?: string;
}
/**
* 营养成分分析服务
* 负责处理食物营养成分表的AI分析
*
* 支持多种AI模型
* - GLM-4.5V (智谱AI) - 设置 AI_VISION_PROVIDER=glm
* - Qwen VL (阿里云DashScope) - 设置 AI_VISION_PROVIDER=dashscope (默认)
*/
@Injectable()
export class NutritionAnalysisService {
private readonly logger = new Logger(NutritionAnalysisService.name);
private readonly client: OpenAI;
private readonly visionModel: string;
private readonly apiProvider: string;
constructor(private readonly configService: ConfigService) {
// Support both GLM-4.5V and DashScope (Qwen) models
this.apiProvider = this.configService.get<string>('AI_VISION_PROVIDER') || 'dashscope';
if (this.apiProvider === 'glm') {
// GLM-4.5V Configuration
const glmApiKey = this.configService.get<string>('GLM_API_KEY');
const glmBaseURL = this.configService.get<string>('GLM_BASE_URL') || 'https://open.bigmodel.cn/api/paas/v4';
this.client = new OpenAI({
apiKey: glmApiKey,
baseURL: glmBaseURL,
});
this.visionModel = this.configService.get<string>('GLM_VISION_MODEL') || 'glm-4v-plus';
} else {
// DashScope Configuration (default)
const dashScopeApiKey = this.configService.get<string>('DASHSCOPE_API_KEY') || 'sk-e3ff4494c2f1463a8910d5b3d05d3143';
const baseURL = this.configService.get<string>('DASHSCOPE_BASE_URL') || 'https://dashscope.aliyuncs.com/compatible-mode/v1';
this.client = new OpenAI({
apiKey: dashScopeApiKey,
baseURL,
});
this.visionModel = this.configService.get<string>('DASHSCOPE_VISION_MODEL') || 'qwen-vl-max';
}
}
/**
* 分析食物营养成分表图片
* @param imageUrl 图片URL
* @returns 营养成分分析结果
*/
async analyzeNutritionImage(imageUrl: string): Promise<NutritionAnalysisResponse> {
try {
this.logger.log(`开始分析营养成分表图片: ${imageUrl}`);
const prompt = this.buildNutritionAnalysisPrompt();
const completion = await this.makeVisionApiCall(prompt, [imageUrl]);
const rawResult = completion.choices?.[0]?.message?.content || '[]';
this.logger.log(`营养成分分析原始结果: ${rawResult}`);
return this.parseNutritionAnalysisResult(rawResult);
} catch (error) {
this.logger.error(`营养成分表分析失败: ${error instanceof Error ? error.message : String(error)}`);
return {
success: false,
data: [],
message: '营养成分表分析失败,请稍后重试'
};
}
}
/**
* 制作视觉模型API调用 - 兼容GLM-4.5V和DashScope
* @param prompt 提示文本
* @param imageUrls 图片URL数组
* @returns API响应
*/
private async makeVisionApiCall(prompt: string, imageUrls: string[]) {
const baseParams = {
model: this.visionModel,
temperature: 0.3,
response_format: { type: 'json_object' } as any,
};
// 处理图片URL
const processedImages = imageUrls.map((imageUrl) => ({
type: 'image_url',
image_url: { url: imageUrl } as any,
}));
if (this.apiProvider === 'glm') {
// GLM-4.5V format
return await this.client.chat.completions.create({
...baseParams,
messages: [
{
role: 'user',
content: [
{ type: 'text', text: prompt },
...processedImages,
] as any,
},
],
} as any);
} else {
// DashScope format (default)
return await this.client.chat.completions.create({
...baseParams,
messages: [
{
role: 'user',
content: [
{ type: 'text', text: prompt },
...processedImages,
] as any,
},
],
});
}
}
/**
* 构建营养成分分析提示
* @returns 提示文本
*/
private buildNutritionAnalysisPrompt(): string {
return `作为专业的营养分析师,请仔细分析这张图片中的营养成分表。
**任务要求:**
1. 识别图片中的营养成分表,提取所有可见的营养素信息
2. 为每个营养素提供详细的健康建议和分析
3. 返回严格的JSON数组格式不包含任何额外的解释或对话文本
**输出格式要求:**
请严格按照以下JSON数组格式返回每个对象包含四个字段
[
{
"key": "energy_kcal",
"name": "热量",
"value": "840千焦",
"analysis": "840千焦约等于201卡路里占成人每日推荐摄入总热量的10%,属于中等热量水平。"
},
{
"key": "protein",
"name": "蛋白质",
"value": "12.5g",
"analysis": "12.5克蛋白质占成人每日推荐摄入量的21%,是良好的蛋白质来源,有助于肌肉修复和生长。"
}
]
**营养素标识符对照表:**
- 热量/能量: energy_kcal
- 蛋白质: protein
- 脂肪: fat
- 碳水化合物: carbohydrate
- 膳食纤维: fiber
- 钠: sodium
- 钙: calcium
- 铁: iron
- 锌: zinc
- 维生素C: vitamin_c
- 维生素A: vitamin_a
- 维生素D: vitamin_d
- 维生素E: vitamin_e
- 维生素B1: vitamin_b1
- 维生素B2: vitamin_b2
- 维生素B6: vitamin_b6
- 维生素B12: vitamin_b12
- 叶酸: folic_acid
- 胆固醇: cholesterol
- 饱和脂肪: saturated_fat
- 反式脂肪: trans_fat
- 糖: sugar
- 其他营养素: other_nutrient
**分析要求:**
1. 如果图片中没有营养成分表,返回空数组 []
2. 为每个识别到的营养素提供具体的健康建议
3. 建议应包含营养素的作用、摄入量参考和健康影响
4. 数值分析要准确,建议要专业且实用
5. 只返回JSON数组不要包含任何其他文本
**重要提醒:**
- 严格按照JSON数组格式返回
- 不要添加任何解释性文字或对话内容
- 确保JSON格式正确可以被直接解析`;
}
/**
* 解析营养成分分析结果
* @param rawResult 原始结果字符串
* @returns 解析后的分析结果
*/
private parseNutritionAnalysisResult(rawResult: string): NutritionAnalysisResponse {
try {
// 尝试解析JSON
let parsedResult: any;
try {
parsedResult = JSON.parse(rawResult);
} catch (parseError) {
this.logger.error(`营养成分分析JSON解析失败: ${parseError}`);
this.logger.error(`原始结果: ${rawResult}`);
return {
success: false,
data: [],
message: '营养成分表解析失败,无法识别有效的营养信息'
};
}
// 确保结果是数组
if (!Array.isArray(parsedResult)) {
this.logger.error(`营养成分分析结果不是数组格式: ${typeof parsedResult}`);
return {
success: false,
data: [],
message: '营养成分表格式错误,无法识别有效的营养信息'
};
}
// 验证和标准化每个营养素项
const nutritionData: NutritionAnalysisResult[] = [];
for (const item of parsedResult) {
if (item && typeof item === 'object' && item.key && item.name && item.value && item.analysis) {
nutritionData.push({
key: String(item.key).trim(),
name: String(item.name).trim(),
value: String(item.value).trim(),
analysis: String(item.analysis).trim()
});
} else {
this.logger.warn(`跳过无效的营养素项: ${JSON.stringify(item)}`);
}
}
if (nutritionData.length === 0) {
return {
success: false,
data: [],
message: '图片中未检测到有效的营养成分表信息'
};
}
this.logger.log(`成功解析 ${nutritionData.length} 项营养素信息`);
return {
success: true,
data: nutritionData
};
} catch (error) {
this.logger.error(`营养成分分析结果处理失败: ${error instanceof Error ? error.message : String(error)}`);
return {
success: false,
data: [],
message: '营养成分表处理失败,请稍后重试'
};
}
}
}