慧视项目的图片上传与前后端联通实现
实现代码
后端
核心思想
- 流式推理驱动的低延迟体验
接口使用流式响应(SSE)逐步推送模型生成的文本片段,避免等待完整结果,提高交互性。系统在首个字符到达时记录首字延迟,用于衡量实时性。 - 任务链路一体化
一次请求同时完成图像上传、模型调用、流式响应和性能指标记录,形成闭环链路:输入 → 推理 → 输出 → 指标回写。 - 可量化的性能监控
每次请求都会记录首字延迟与总延迟,写入数据库,实现真实线上性能可追踪,可用于 SLA 与优化分析。 - 轻量化本地持久化
使用 SQLite 作为本地日志存储,降低部署成本,便于快速验证和调试,也能平滑迁移到更重型数据库。
代码逻辑
- 应用初始化
• 创建 FastAPI 实例。
• 启动时调用 init_db() 初始化数据库表。
• 设置 dashscope.api_key。 - 接口 /v1/vision/analyze
• 生成 request_id 并记录起始时间。
• 读取上传图片,校验非空,转为 Base64。 - 流式生成 event_generator
• 调用 DashScope 多模态模型,开启 stream=True。
• 循环读取流式返回:
• 取出当前累计文本。
• 计算新增内容并 yield 给客户端。
• 第一次有新增内容时记录首字时间。 - 统计与落库
• 流结束后计算:
• 首字延迟 first_latency
• 总延迟 total_latency
• 写入 VisionLog 表(请求 ID、图片信息、AI 结果、延迟指标)。 - 响应
• 返回 StreamingResponse,媒体类型为 text/event-stream。
前端
拍照上传
在环境配置的 envList.js 中新增后端地址配置
先用快速分析测试前后端联通
流程是“创建相机控制器 → 拍照 → 成功就拿到图片路径并上传分析,失败就提示用户”。
核心思想是将“拍照识别”做成一个前端到后端的异步流水线:先用小程序相机拍照,再把照片上传给 AI 服务处理,最后把识别结果反馈给用户。整体流程是事件驱动的,用户点一次按钮,就触发一次完整的采集、上传、识别、展示。
代码逻辑:
quickAnalysis()负责发起拍照。它先创建相机上下文wx.createCameraContext(),然后调用takePhoto()。- 拍照成功后,微信会返回临时图片路径
tempImagePath,代码把这个路径交给uploadAndAnalyze(filePath)。 uploadAndAnalyze()先显示“正在识别…”的加载提示,告诉用户当前在处理。- 然后通过
wx.uploadFile()把图片上传到后端接口/v1/vision/analyze。 formData里带了mode: this.data.activeMode,这表示当前识别模式会一起传给后端,后端可以据此决定按“会视”还是“出行”逻辑来分析。- 后端返回结果后,前端用
JSON.parse(res.data)解析响应,取出result.description。 - 最后用
wx.showModal()把识别结果展示给用户。 - 无论成功还是失败,
complete都会执行wx.hideLoading(),保证加载态被清理掉。
消息发送功能
核心思想是把用户输入的文字当作“对话请求”,校验后发给后端聊天接口,并在发送成功后清空输入框,形成一个最小闭环的消息发送流程。
代码逻辑:
- 先读取输入框内容
inputValue,并用trim()去掉首尾空白。 - 如果内容为空,直接返回,避免发出无效请求。
- 通过
wx.request()向${API_URL}/api/chat发送一个 POST 请求。 - 请求体里带了两个字段:
query是用户输入内容,mode是当前页面模式activeMode。
前后端联通测试
本机测试
后端:uvicorn.run(app, host="0.0.0.0", port=8000)
前端:API_URL = "http://127.0.0.1:8000"
局域网测试
电脑连接手机 Wifi,关闭防火墙。
将前端的 API_URL 中的 127.0.0.1 改成上一步中的地址。
内网穿透
使用平台贝锐蒲公英
源码
后端:
@app.post("/v1/vision/analyze") async def analyze_scene(file: UploadFile = File(...)): request_id = str(uuid.uuid4()) start_time = time.time() content = await file.read() if not content: raise HTTPException(status_code=400, detail="图片上传失败") base64_image = base64.b64encode(content).decode("utf-8") async def event_generator(): full_content = "" first_token_time = None responses = dashscope.MultiModalConversation.call( model="qwen-vl-plus", messages=[{"role": "user", "content": [ {"image": f"data:image/jpeg;base64,{base64_image}"}, {"text": "你是一位视障人士向导。请简洁描述正前方2米内的障碍物及方位。"} ]}], stream=True ) for response in responses: if response.status_code == 200: current_full_text = response.output.choices[0].message.content[0]["text"] new_content = current_full_text[len(full_content):] full_content = current_full_text if not first_token_time and new_content: first_token_time = time.time() if new_content: yield new_content else: yield f"Error: {response.message}" end_time = time.time() first_latency = (first_token_time - start_time) * 1000 if first_token_time else 0 total_latency = (end_time - start_time) * 1000 db = SessionLocal() try: log_entry = VisionLog( request_id=request_id, image_path=file.filename, ai_result=full_content, first_token_latency=first_latency, total_latency=total_latency ) db.add(log_entry) db.commit() print(f"日志已存库: ReqID={request_id}, 首字延迟={first_latency:.2f}ms") except Exception as e: print(f"数据库写入失败: {e}") finally: db.close() return StreamingResponse(event_generator(), media_type="text/event-stream")前端:
index.js
// 流程是“创建相机控制器 → 拍照 → 成功就拿到图片路径并上传分析,失败就提示用户”。 quickAnalysis() { const ctx = wx.createCameraContext(); ctx.takePhoto({ quality: 'normal', success: (res) => { const tempFilePath = res.tempImagePath; this.uploadAndAnalyze(tempFilePath); // 调用上传函数 }, fail: () => { wx.showToast({ title: '拍照失败', icon: 'none' }); } }); }, // 发送图片到后端进行 AI 分析 uploadAndAnalyze(filePath) { wx.showLoading({ title: '正在识别...' }); wx.uploadFile({ url: `${API_URL}/v1/vision/analyze`, // 后端接口路径 filePath: filePath, name: 'file', // 后端接收文件的字段名 formData: { 'mode': this.data.activeMode // 传入当前模式:出行或会视 }, success: (res) => { const result = JSON.parse(res.data); console.log('分析结果:', result.description); wx.showModal({ title: '识别结果', content: result.description, showCancel: false }); }, fail: (err) => { console.error('联调失败:', err); wx.showToast({ title: '网络请求失败', icon: 'none' }); }, complete: () => { wx.hideLoading(); } }); }, // 发送消息 sendMessage() { const value = this.data.inputValue.trim(); if (!value) return; wx.request({ url: `${API_URL}/api/chat`, // 后端接口路径 method: 'POST', data: { query: value, mode: this.data.activeMode }, success: (res) => { // 处理对话结果 this.setData({ inputValue: '' }); } }); },vision.js:
// 拍照功能 takePhoto() { const ctx = wx.createCameraContext(); ctx.takePhoto({ quality: 'high', success: (res) => { console.log('照片路径:', res.tempImagePath); // 联调关键:将照片上传至后端 wx.uploadFile({ url: `${API_URL}/api/vision-identify`, // 后端接口路径 filePath: res.tempImagePath, name: 'image', success: (uploadRes) => { wx.showToast({ title: '识别中...', icon: 'loading' }); // 处理识别逻辑 } }); }, fail: (err) => { console.error(err); wx.showToast({ title: '拍照失败', icon: 'none' }); } }); }