开发日志(二):实现菜单图片智能解析、翻译与前端展示,并扩展菜品详情分析能力
一、阶段目标:从“识别菜单”走向“理解菜品”
上一篇开发日志中,我完成了项目的基础框架搭建,包括 Flutter 前端、FastAPI 后端、MySQL 数据库以及前后端基础联通。那一阶段的重点,是把系统从 0 到 1 跑起来。
而这一阶段,我推进的是项目真正的核心功能:
让用户上传一张菜单图片,系统自动完成识别、翻译、结构化解析,并在前端进行可交互展示。
并且,这一阶段不只是完成了菜单列表的解析展示,我还进一步扩展了菜品详情页的二次分析能力:
用户点击某一道菜后,前端会再次调用后端 API,获取该菜品更细粒度的分析信息,例如主要原材料、过敏原、口味特征、菜系类型、烹饪方法、营养信息和饮食限制等。
这意味着,系统已经不只是“把菜单翻译出来”,而是在向“智能点餐助手”真正靠近。
二、本阶段核心成果
这一阶段,我完成了项目目前最重要的一条业务链路:
菜单图片上传 → 模型解析翻译 → 菜品列表展示 → 菜品详情二次分析
具体来说,我完成了以下功能:
-
✅ 后端接入 Qwen 多模态模型,直接解析菜单图片
-
✅ 输出菜品名称、中文翻译、描述、价格、标签等结构化信息
-
✅ 前端展示菜品解析结果列表
-
✅ 前端支持点击菜品进入详情页
-
✅ 详情页再次调用 API,获取菜品深度分析信息
-
✅ 展示原材料、过敏原、口味特征、菜系类型、烹饪方法、营养信息等内容
-
✅ 修复上传报错与前端解析失败问题,打通完整链路
相比上一篇“框架搭建”的阶段,这一阶段最大的不同在于:
系统已经具备了一个完整、可演示、可交互的核心业务能力。
三、本阶段最关键的实现:调用模型对菜单图片进行解析与翻译
这一阶段最重要的,不是单纯完成图片上传,而是实现了:
调用模型对菜单图片进行识别、翻译和结构化理解。
以前如果只是用传统 OCR,往往只能拿到一段原始菜单文本,信息杂乱,前端也很难直接展示。
而现在,我在后端直接调用 Qwen 多模态模型,让它完成更高层次的任务:
-
识别菜单中的菜品内容
-
翻译菜品名称和描述
-
提取价格信息
-
概括菜品标签
-
按统一字段输出结构化结果
后端上传接口的核心逻辑如下:
@app.post("/upload")
async def upload_file(file: UploadFile = File(...)):
contents = await file.read()
if not file.content_type or not file.content_type.startswith("image/"):
raise HTTPException(status_code=400, detail="请上传图片文件")
parsed_result = parse_menu_image_with_qwen(contents, file.content_type)
return {
"success": True,
"message": "解析成功",
"items": parsed_result.get("items", [])
}
这里有两个关键点:
-
上传接口不再只是接收文件,而是直接进入模型解析流程。
-
后端不再依赖 Tesseract OCR,而是直接调用 Qwen 多模态模型完成图片理解。
这一点对项目来说非常重要,因为它让菜单解析从“文本提取”升级成了“语义理解”。
四、结构化返回:让模型输出真正能被前端消费
为了让前端稳定展示解析结果,我将后端输出统一为标准 JSON 结构,例如:
{
"success": true,
"message": "解析成功",
"items": [
{
"name_original": "Two-Roll Special",
"name_zh": "双卷特惠",
"description": "包含两种寿司卷,可选加州卷、JB卷、三文鱼卷、熟三文鱼卷、辣金枪鱼卷或蔬菜卷,附汤或沙拉。",
"price": "$12.95",
"tags": ["寿司", "特惠"]
}
]
}
这一步看起来只是“定义了几个字段”,但实际上非常关键。
因为只有把模型输出变成稳定的结构化格式,前端才能真正完成:
-
列表展示
-
详情展示
-
跳转交互
-
后续收藏、推荐、问答等扩展
也就是说,这一阶段我做的不只是“把结果返回出来”,而是:
把模型生成结果转化成了前端可直接使用的数据接口。
五、前端展示:从菜单图片到菜品列表页
在 Flutter 前端,我完成了菜单解析结果的列表展示。
上传菜单图片后,前端会请求后端 /upload 接口,拿到结构化菜品数据,然后渲染为菜品卡片列表。每张卡片展示:
-
原文菜名
-
中文菜名
-
描述
-
价格
-
标签
前端上传与解析的关键逻辑如下:
final response = await _uploadImageToBackend(
bytes: bytes,
filename: file.name,
);
final data = jsonDecode(response.body);
if (data['success'] == true && data['items'] != null) {
final parsedItems = (data['items'] as List)
.map((item) => MenuItem.fromJson(item))
.toList();
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => ResultsPage(parsedItems: parsedItems),
),
);
}
同时,前端 MenuItem 模型也与后端字段保持一致:
class MenuItem {
final String? nameOriginal;
final String? nameZh;
final String? description;
final String? price;
final List<String>? tags;
factory MenuItem.fromJson(Map<String, dynamic> json) {
return MenuItem(
nameOriginal: json['name_original'],
nameZh: json['name_zh'],
description: json['description'],
price: json['price']?.toString(),
tags: json['tags'] != null ? List<String>.from(json['tags']) : null,
);
}
}
这一部分完成后,系统已经可以实现:
用户上传一张菜单图,前端直接展示出可读的中文菜品列表。

六、进一步扩展:菜品详情页不再只是静态展示,而是再次调用 API 进行深度分析
这一阶段我觉得最有价值的增强,不只是列表页,而是详情页的能力升级。
最开始,详情页只能展示在列表阶段已经拿到的基础信息,例如:
-
中文名称
-
原始英文名称
-
价格
-
描述
-
标签
但这还远远不够。
因为用户在点餐时,往往更关心的是:
-
这道菜主要是由什么做成的?
-
有没有我过敏的成分?
-
口味偏甜还是偏咸?
-
属于什么菜系?
-
是烤的、炸的还是煮的?
-
营养特点是什么?
-
适不适合某种饮食限制?
因此,我在后端新增了一个接口:
/dish-details:对单道菜进行深度分析
接口核心逻辑如下:
@app.post("/dish-details")
async def get_dish_details(request: dict):
name_zh = request.get("name_zh")
name_original = request.get("name_original")
if not name_zh:
raise HTTPException(status_code=400, detail="菜品中文名不能为空")
details_result = get_dish_details_with_qwen(name_zh, name_original)
return details_result
也就是说,当用户点击某道菜进入详情页时,系统会基于该菜品名称,再调用一次模型,让模型对这道菜进行更深入的语义分析,而不是只停留在菜单表层信息。
七、详情页的前端实现:按需加载更多菜品信息
在 Flutter 前端,详情页增加了一个“获取详细分析”按钮。点击后,前端会向 /dish-details 发送请求,请求体中包含:
-
name_zh
-
name_original
核心请求逻辑如下:
final response = await http.post(
Uri.parse('http://127.0.0.1:8000/dish-details'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'name_zh': widget.menuItem.nameZh,
'name_original': widget.menuItem.nameOriginal,
}),
);
如果返回成功,页面会展示详细分析结果。
这一步的意义非常大,因为它让详情页变成了一个按需扩展的智能分析页面,而不只是一个静态信息页。
八、详情页展示了哪些新增信息?
目前,菜品详情页已经能够展示以下深度分析内容:
1. 主要原材料
例如生菜、面包丁、帕玛森奶酪、凯撒沙拉酱、烤鸡肉、烤虾等。
2. 过敏原信息
例如乳制品、贝类、小麦等。
3. 口味特征
我把模型返回的口味特征做成了可视化进度条,包括:
-
甜
-
酸
-
咸
-
苦
-
辣
-
鲜
每项以 0~10 分表示,用户可以更直观看出菜品风味倾向。
4. 菜系类型
例如西餐、日式等。
5. 烹饪方法
例如烤、炸、煮等。
6. 营养信息
例如高蛋白、低脂、富含维生素和矿物质等。
7. 饮食限制
例如是否适合素食、是否含麸质、是否含海鲜等。
这部分内容已经明显超出了普通“菜单翻译”的范围,更接近一个真正的智能菜品理解系统。
九、开发过程中遇到的两个典型问题
虽然这篇日志的重点是“实现菜单解析与详情分析”,但在联调过程中,也遇到了两个必须解决的问题。
1. Flutter 上传图片时偶发 400
问题原因是前端最开始把图片类型写死成了 image/jpeg,上传 PNG、WebP 等图片时,后端可能识别为 application/octet-stream,进而校验失败。
后来我改成了根据文件后缀动态设置 MIME 类型:
late MediaType mediaType;
if (filename.toLowerCase().endsWith('.png')) {
mediaType = MediaType('image', 'png');
} else if (filename.toLowerCase().endsWith('.webp')) {
mediaType = MediaType('image', 'webp');
} else {
mediaType = MediaType('image', 'jpeg');
}
这样之后,上传稳定性就明显提高了。
2. 前端解析后端返回数据失败

另一个问题是前端一开始没有适配后端的 JSON 结构,尤其是:
-
外层的 {success, message, items}
-
字段名 name_original、name_zh
后来通过调整前端解析逻辑和 MenuItem.fromJson() 映射,问题得到解决。
这两个问题本身不是本阶段的最终目标,但确实是把整条链路打通必须跨过去的环节。
十、这一阶段真正完成了什么?
我真正打通了菜单图片解析、翻译、前端展示,以及菜品详情二次分析这整条业务链路。
也就是说,系统现在已经可以做到:
用户上传菜单图片
→ 后端调用 Qwen 解析和翻译菜单
→ 前端展示菜品列表
→ 用户点击菜品进入详情页
→ 前端再次调用 API
→ 后端基于菜品名称返回更深入的分析信息
→ 前端完成详细可视化展示
这已经不是单一接口调通,而是一个完整的交互闭环。
本阶段我主要完成的工作
-
设计并实现菜单图片上传流程
-
后端接入 Qwen 多模态模型解析菜单图片
-
设计菜品结构化返回格式
-
完成 Flutter 菜品列表页展示
-
实现详情页跳转与基础信息展示
-
新增 /dish-details 接口,支持菜品深度分析
-
完成详情页对二次分析结果的展示与可视化
-
修复上传类型错误和前端 JSON 解析问题
-
打通从“图片输入”到“详情分析”的完整流程
十一、当前系统能力总结
截至目前,系统已经具备以下能力:
前端
-
上传菜单图片
-
展示菜品列表
-
展示菜品详情
-
详情页按需获取更多分析信息
-
可视化展示口味特征、原材料、过敏原等内容
后端
-
接收菜单图片
-
调用 Qwen 多模态模型解析菜单
-
翻译并结构化输出菜品信息
-
提供单道菜深度分析接口
-
返回更细粒度的语义信息
整体流程
-
菜单图片输入
-
模型解析与翻译
-
菜品结构化输出
-
前端列表展示
-
菜品详情二次分析
-
结果可视化呈现
这意味着,项目已经具备了一个比较完整的“智能菜单理解与点餐辅助”雏形。
十二、下一步计划
接下来,我会继续在这一基础上做增强,主要包括:
-
提高复杂菜单图片的解析准确率
-
优化模型返回结果的一致性和稳定性
-
增加菜品推荐与用户偏好联动
-
加入 AI 问答能力,例如“这道菜适合减脂吗”“有没有不含海鲜的替代选择”
-
进一步完善收藏、推荐和个性化点餐功能
更多推荐




所有评论(0)