feat(catface): 优化猫脸识别功能

- 新增猫脸识别失败的错误码和错误信息
- 重构猫脸识别结果处理逻辑,增加对结果为空的处理
- 优化猫脸识别结果展示,包括品种翻译和动物信息展示
- 新增测试用例,验证猫脸识别功能正常工作
This commit is contained in:
Havoc412 2024-11-23 01:30:13 +08:00
parent d04987302f
commit 609e02dfd2
11 changed files with 192 additions and 117 deletions

View File

@ -3,13 +3,19 @@ package errcode
const (
ErrAnimalSqlFind = iota + ErrAnimal
AnimalNoFind
// TAG
CatFaceFail
// CatFaceNoFind // INFO 交给前端判断更合适
)
func AnimalMsgInit(m msg) {
m[ErrAnimalSqlFind] = "Animals 表单查询失败"
m[AnimalNoFind] = "Animals 没有查询到符合条件的目标"
m[CatFaceFail] = "猫脸识别失败"
}
func AnimalMsgUserInit(m msg) {
m[AnimalNoFind] = "没有更多符合此条件的毛茸茸啦,试着更换查询条件或者新增吧~"
m[CatFaceFail] = "猫脸识别失败,请重新尝试~ 😿"
}

View File

@ -8,6 +8,7 @@ import (
"catface/app/model"
"catface/app/model_es"
"catface/app/model_redis"
"catface/app/model_res"
"catface/app/service/animals/curd"
"catface/app/service/catface"
"catface/app/service/upload_file"
@ -26,32 +27,52 @@ type Animals struct { // INFO 起到一个标记的作用,这样 web.xxx 的
func (a *Animals) Guess(context *gin.Context) {
// 1. Get Params
filePath := context.GetString(consts.ValidatorPrefix + "file_path")
// 2. Get Result
catRes := catface.GetCatfaceResult(filePath)
// 3. Response
filePath = filepath.Join(variable.ConfigYml.GetString("FileUploadSetting.UploadFileSavePath"), variable.ConfigYml.GetString("FileUploadSetting.CatFaceTempRootPath"), filePath)
type subT struct {
Id int64 `json:"id"`
Name string `json:"name"`
Status uint8 `json:"status"`
Department uint8 `json:"department"`
// STAGE - 2 Get Result From Python API
catRes, err := catface.GetCatfaceResult(filePath)
if err != nil {
response.Fail(context, errcode.CatFaceFail, errcode.ErrMsg[errcode.CatFaceFail], errcode.ErrMsgForUser[errcode.CatFaceFail])
return
}
type t struct {
List []subT `json:"list"`
}
// STAGE - 3
var resList t
for _, v := range catRes.Cats {
resList.List = append(resList.List, subT{
Id: v.Id,
Name: model.CreateAnimalFactory("").ShowByID(v.Id).Name,
Status: model.CreateAnimalFactory("").ShowByID(v.Id).Status,
Department: model.CreateAnimalFactory("").ShowByID(v.Id).Department,
// FaceBreed: En -> Zh
faceBreedZh := model.CreateAnmBreedFactory("").GetNameZhByEn(catRes.FaceBreed)
// CatFace 返回为空,直接返回。
if len(catRes.Cats) == 0 {
response.Success(context, consts.CurdStatusOkMsg, gin.H{
"face_breed": faceBreedZh,
"animals": nil,
})
return
}
// fill other information
var ids []int64
for _, v := range catRes.Cats {
ids = append(ids, v.Id)
}
response.Success(context, consts.CurdStatusOkMsg, resList)
animals := model.CreateAnimalFactory("").ShowByIDs(ids, "id", "name", "status", "department", "nick_names", "description", "avatar")
var res []model_res.CatfaceCat
for _, animal := range animals {
for _, cat := range catRes.Cats {
if cat.Id == animal.Id {
res = append(res, model_res.CatfaceCat{
Animal: animal,
Conf: cat.Conf,
})
}
}
}
response.Success(context, consts.CurdStatusOkMsg, gin.H{
"face_breed": faceBreedZh,
"animals": res,
})
}
func (a *Animals) List(context *gin.Context) {

View File

@ -5,10 +5,12 @@ import (
"catface/app/http/controller/web"
"catface/app/http/validator/core/data_transfer"
"catface/app/utils/response"
"github.com/gin-gonic/gin"
)
type CatfaceGuess struct {
FilePath string `form:"file_path" json:"file_path" binding:"required"`
}
func (c CatfaceGuess) CheckParams(context *gin.Context) {

View File

@ -1,11 +1,6 @@
package model
// INFO 一些基础表单的整合
type AnmBreed struct {
BriefModel
}
// TAG 一些基础表单的整合
type AnmSterilzation struct { // TEST How to use BriefModel, the dif between Common
Id int64 `json:"id"`
NameZh string `json:"name_zh"`
@ -28,3 +23,24 @@ type AnmVaccination struct {
type AnmDeworming struct {
BriefModel
}
// TAG 带了点函数处理
func CreateAnmBreedFactory(sqlType string) *AnmBreed {
return &AnmBreed{BriefModel: BriefModel{DB: UseDbConn(sqlType)}}
}
type AnmBreed struct {
BriefModel
}
func (a *AnmBreed) TableName() string {
return "anm_breeds"
}
func (a *AnmBreed) GetNameZhByEn(name_en string) string {
var temp AnmBreed
if result := a.DB.Where("name_en = ?", name_en).First(&temp); result.Error != nil {
return "" // 如果查询失败,返回空字符串
}
return temp.NameZh
}

View File

@ -20,9 +20,10 @@ type BaseModel struct {
}
type BriefModel struct {
Id int64 `json:"id"`
NameZh string `json:"name_zh"`
NameEn string `json:"name_en"`
*gorm.DB `gorm:"-" json:"-"`
Id int64 `json:"id"`
NameZh string `json:"name_zh"`
NameEn string `json:"name_en"`
}
type Color struct {

8
app/model_res/catface.go Normal file
View File

@ -0,0 +1,8 @@
package model_res
import "catface/app/model"
type CatfaceCat struct {
Animal model.Animal `json:"animal"`
Conf float64 `json:"conf"`
}

View File

@ -4,29 +4,42 @@ import (
"catface/app/global/variable"
"catface/app/utils/micro_service"
"context"
"fmt"
"github.com/carlmjohnson/requests"
)
type FaceRes struct {
FaceBreed int `json:"face_breed"`
type FaceData struct {
FaceBreed string `json:"face_breed"`
Cats []struct {
Id int64 `json:"id"`
Prob float64 `json:"prob"`
Conf float64 `json:"conf"`
} `json:"cats"`
}
func GetCatfaceResult(filePath string) FaceRes {
type CatFaceRes struct {
Status int `json:"status"`
Data FaceData `json:"data"`
}
func GetCatfaceResult(filePath string) (*FaceData, error) {
body := map[string]interface{}{
"file_path": filePath,
}
var res FaceRes
var res CatFaceRes
err := requests.URL(micro_service.FetchPythonServiceUrl("cnn/detect_cat")).
BodyJSON(&body).
ToJSON(&res).
Fetch(context.Background())
if err != nil {
variable.ZapLog.Error("获取cat face结果集失败: " + err.Error())
return nil, err
}
return res
if res.Status != 200 {
return nil, fmt.Errorf("请求状态码错误: %d", res.Status)
}
return &res.Data, nil
}

View File

@ -79,6 +79,7 @@ FileUploadSetting:
AvatarWidth: 200
DocsRootPath: "docs" # TODO 或许 upload 模块可以写一下自动区分大致的文件类型所在的位置。
CatFaceTempRootPath: "catfaceTemp"
# casbin 权限控制api接口
Casbin:

View File

@ -42,5 +42,5 @@ func TestExplain(t *testing.T) {
}
// 调用 StructToString 函数
t.Logf("结构体内容:", StructToString(encounter))
t.Logf("结构体内容:%v", StructToString(encounter))
}

View File

@ -2,6 +2,7 @@ package test
import (
"catface/app/model"
_ "catface/bootstrap"
"testing"
)
@ -14,3 +15,8 @@ func TestDocModel(t *testing.T) {
t.Error(err)
}
}
func TestAnmBreed(t *testing.T) {
res := model.CreateAnmBreedFactory("").GetNameZhByEn("li")
t.Log(res)
}

View File

@ -1,97 +1,98 @@
//后端代码
// //后端代码
//注意 **我注释的代码是不使用gin框架封装的Stream方法也就是C.Stream(func())和C.ssevent(),只是C.Stream要改成for循环持续的从通道里面进行读直到通道关闭结束for循环**
// //注意 **我注释的代码是不使用gin框架封装的Stream方法也就是C.Stream(func())和C.ssevent(),只是C.Stream要改成for循环持续的从通道里面进行读直到通道关闭结束for循环**
package main
import (
"catface/app/service/nlp/glm"
_ "catface/bootstrap"
"fmt"
"io"
"testing"
// "time"
// import (
// "catface/app/service/nlp/glm"
// _ "catface/bootstrap"
// "fmt"
// "io"
// "testing"
// // "time"
//
"github.com/gin-gonic/gin"
// "github.com/gin-gonic/gin"
)
// )
func SSE(c *gin.Context) {
// 设置响应头告诉前端适用event-stream事件流交互
//c.Writer.Header().Set("Content-Type", "text/event-stream")
//c.Writer.Header().Set("Cache-Control", "no-cache")
//c.Writer.Header().Set("Connection", "keep-alive")
// func SSE(c *gin.Context) {
// // 设置响应头告诉前端适用event-stream事件流交互
// //c.Writer.Header().Set("Content-Type", "text/event-stream")
// //c.Writer.Header().Set("Cache-Control", "no-cache")
// //c.Writer.Header().Set("Connection", "keep-alive")
// 判断是否支持sse
//w := c.Writer
//flusher, _ := w.(http.Flusher)
query := c.Query("query")
// // 判断是否支持sse
// //w := c.Writer
// //flusher, _ := w.(http.Flusher)
// query := c.Query("query")
// 接收前端页面关闭连接通知
closeNotify := c.Request.Context().Done()
// // 接收前端页面关闭连接通知
// closeNotify := c.Request.Context().Done()
// 开启协程监听前端页面是否关闭了连接,关闭连接会触发此方法
go func() {
<-closeNotify
fmt.Println("SSE关闭了")
return
}()
// // 开启协程监听前端页面是否关闭了连接,关闭连接会触发此方法
// go func() {
// <-closeNotify
// fmt.Println("SSE关闭了")
// return
// }()
//新建一个通道,用于数据接收和响应
Chan := make(chan string)
// //新建一个通道,用于数据接收和响应
// Chan := make(chan string)
// 异步接收GPT响应然后把响应的数据发送到通道Chan
go func() {
err := glm.ChatStream(query, Chan)
if err != nil {
fmt.Println("Error", err)
}
// // 异步接收GPT响应然后把响应的数据发送到通道Chan
// go func() {
// err := glm.ChatStream(query, Chan)
// if err != nil {
// fmt.Println("Error", err)
// }
close(Chan)
}()
// close(Chan)
// }()
// gin框架封装的stream,会持续的调用这个func方法记得返回true;返回false代表结束调用func方法
c.Stream(func(w io.Writer) bool {
select {
case i, ok := <-Chan:
if !ok {
return false
}
c.SSEvent("chat", i) // c.SSEvent会自动修改响应头为事件流并发送”test“事件流给前端监听”test“的回调方法
//flusher.Flush() // 确保立即发送
return true
case <-closeNotify:
fmt.Println("SSE关闭了")
return false
}
})
}
// // gin框架封装的stream,会持续的调用这个func方法记得返回true;返回false代表结束调用func方法
// c.Stream(func(w io.Writer) bool {
// select {
// case i, ok := <-Chan:
// if !ok {
// return false
// }
// c.SSEvent("chat", i) // c.SSEvent会自动修改响应头为事件流并发送”test“事件流给前端监听”test“的回调方法
// //flusher.Flush() // 确保立即发送
// return true
// case <-closeNotify:
// fmt.Println("SSE关闭了")
// return false
// }
// })
// }
func TestSSE(t *testing.T) {
engine := gin.Default()
// 设置跨域中间件
engine.Use(func(context *gin.Context) {
origin := context.GetHeader("Origin")
// 允许 Origin 字段中的域发送请求
context.Writer.Header().Add("Access-Control-Allow-Origin", origin) // 这边我的前端页面在63342会涉及跨域这个根据自己情况设置或者直接设置为”*“,放行所有的
// 设置预验请求有效期为 86400 秒
context.Writer.Header().Set("Access-Control-Max-Age", "86400")
// 设置允许请求的方法
context.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, U`PDATE, PATCH")
// 设置允许请求的 Header
context.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Apitoken")
// 设置拿到除基本字段外的其他字段如上面的Apitoken, 这里通过引用Access-Control-Expose-Headers进行配置效果是一样的。
context.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Headers")
// 配置是否可以带认证信息
context.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
// OPTIONS请求返回200
if context.Request.Method == "OPTIONS" {
fmt.Println(context.Request.Header)
context.AbortWithStatus(200)
} else {
context.Next()
}
})
engine.GET("/admin/rag/default_talk", SSE) // TIP 记得适用get请求我用post前端报404资料说是SSE只支持get请求
engine.Run(":20201")
}
// func TestSSE(t *testing.T) {
// engine := gin.Default()
// // 设置跨域中间件
// engine.Use(func(context *gin.Context) {
// origin := context.GetHeader("Origin")
// // 允许 Origin 字段中的域发送请求
// context.Writer.Header().Add("Access-Control-Allow-Origin", origin) // 这边我的前端页面在63342会涉及跨域这个根据自己情况设置或者直接设置为”*“,放行所有的
// // 设置预验请求有效期为 86400 秒
// context.Writer.Header().Set("Access-Control-Max-Age", "86400")
// // 设置允许请求的方法
// context.Writer.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, U`PDATE, PATCH")
// // 设置允许请求的 Header
// context.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Apitoken")
// // 设置拿到除基本字段外的其他字段如上面的Apitoken, 这里通过引用Access-Control-Expose-Headers进行配置效果是一样的。
// context.Writer.Header().Set("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Headers")
// // 配置是否可以带认证信息
// context.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
// // OPTIONS请求返回200
// if context.Request.Method == "OPTIONS" {
// fmt.Println(context.Request.Header)
// context.AbortWithStatus(200)
// } else {
// context.Next()
// }
// })
// engine.GET("/admin/rag/default_talk", SSE) // TIP 记得适用get请求我用post前端报404资料说是SSE只支持get请求
// engine.Run(":20201")
// }