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

View File

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

View File

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

View File

@ -1,11 +1,6 @@
package model package model
// INFO 一些基础表单的整合 // TAG 一些基础表单的整合
type AnmBreed struct {
BriefModel
}
type AnmSterilzation struct { // TEST How to use BriefModel, the dif between Common type AnmSterilzation struct { // TEST How to use BriefModel, the dif between Common
Id int64 `json:"id"` Id int64 `json:"id"`
NameZh string `json:"name_zh"` NameZh string `json:"name_zh"`
@ -28,3 +23,24 @@ type AnmVaccination struct {
type AnmDeworming struct { type AnmDeworming struct {
BriefModel 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 { type BriefModel struct {
Id int64 `json:"id"` *gorm.DB `gorm:"-" json:"-"`
NameZh string `json:"name_zh"` Id int64 `json:"id"`
NameEn string `json:"name_en"` NameZh string `json:"name_zh"`
NameEn string `json:"name_en"`
} }
type Color struct { 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/global/variable"
"catface/app/utils/micro_service" "catface/app/utils/micro_service"
"context" "context"
"fmt"
"github.com/carlmjohnson/requests" "github.com/carlmjohnson/requests"
) )
type FaceRes struct { type FaceData struct {
FaceBreed int `json:"face_breed"` FaceBreed string `json:"face_breed"`
Cats []struct { Cats []struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Prob float64 `json:"prob"` Conf float64 `json:"conf"`
} `json:"cats"` } `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{}{ body := map[string]interface{}{
"file_path": filePath, "file_path": filePath,
} }
var res FaceRes var res CatFaceRes
err := requests.URL(micro_service.FetchPythonServiceUrl("cnn/detect_cat")). err := requests.URL(micro_service.FetchPythonServiceUrl("cnn/detect_cat")).
BodyJSON(&body). BodyJSON(&body).
ToJSON(&res). ToJSON(&res).
Fetch(context.Background()) Fetch(context.Background())
if err != nil { if err != nil {
variable.ZapLog.Error("获取cat face结果集失败: " + err.Error()) 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 AvatarWidth: 200
DocsRootPath: "docs" # TODO 或许 upload 模块可以写一下自动区分大致的文件类型所在的位置。 DocsRootPath: "docs" # TODO 或许 upload 模块可以写一下自动区分大致的文件类型所在的位置。
CatFaceTempRootPath: "catfaceTemp"
# casbin 权限控制api接口 # casbin 权限控制api接口
Casbin: Casbin:

View File

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

View File

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