diff --git a/.gitignore b/.gitignore index 394591b..8930961 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ GinSkeleton/.vscode GinSkeleton/.idea/ .idea/ .idea -GinSkeleton/storage/ -GinSkeleton/storage/ ckeditor5/node_modules/* +GinSkeleton/config/gorm_v2.yml +GinSkeleton/public/storage diff --git a/GinSkeleton/api_doc.md b/GinSkeleton/api_doc.md index af4a9d2..505a9b8 100644 --- a/GinSkeleton/api_doc.md +++ b/GinSkeleton/api_doc.md @@ -184,4 +184,22 @@ Authorization|Headers|string|必填|Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. }, "msg": "Success" } +``` + +#### 请求图片文字识别 +> *post*,/admin/ai_recognition/pic_recognition + +参数字段|参数属性|类型|选项|默认值 +---|---|---|---|--- +pic|form-data|string|必填|"" + +> 返回示例: +```json +{ + "code": 200, + "data": { + "words": "out[entry]=\nfor each basic block ∈N -{entry}\nout[B] =\nchange = true\nwhile (changes) {\n// Find: fix point solution\nchange = false\nfor each B∈N - {entry} {\noldout = out[B]\nin[B]= Uout[P]\nP∈pred[B]\nout[B] = GEN[B] ∪ (in[B]∩ PRSV[B])\nif(out[B] ≠ oldout) change = true;\n}\n}\n" + }, + "msg": "Success" +} ``` \ No newline at end of file diff --git a/GinSkeleton/app/global/consts/consts.go b/GinSkeleton/app/global/consts/consts.go index e49ccac..4520742 100644 --- a/GinSkeleton/app/global/consts/consts.go +++ b/GinSkeleton/app/global/consts/consts.go @@ -38,8 +38,8 @@ const ( CurdCreatFailMsg string = "新增失败" CurdUpdateFailCode int = -400201 CurdUpdateFailMsg string = "更新失败" - CurdUpdatePassFailCode int = -400207 - CurdUpdatePassFailMsg string = "更新密码失败" + CurdUpdatePassFailCode int = -400207 + CurdUpdatePassFailMsg string = "更新密码失败" CurdDeleteFailCode int = -400202 CurdDeleteFailMsg string = "删除失败" CurdSelectFailCode int = -400203 @@ -50,10 +50,10 @@ const ( CurdLoginFailMsg string = "登录失败" CurdRefreshTokenFailCode int = -400206 CurdRefreshTokenFailMsg string = "刷新Token失败" - CurdLogoutFailCode int = -400208 - CurdLogoutFailMsg string = "登出失败" - CurdPublicKeyFailCode int = -400209 - CurdPublicKeyFailMsg string = "密钥获取失败" + CurdLogoutFailCode int = -400208 + CurdLogoutFailMsg string = "登出失败" + CurdPublicKeyFailCode int = -400209 + CurdPublicKeyFailMsg string = "密钥获取失败" //文件上传 FilesUploadFailCode int = -400250 FilesUploadFailMsg string = "文件上传失败, 获取上传文件发生错误!" @@ -77,4 +77,8 @@ const ( CaptchaCheckOkMsg string = "验证码校验通过" CaptchaCheckFailCode int = -400355 CaptchaCheckFailMsg string = "验证码校验失败" + + // 模型功能按相关 + PicRecognitionFailMsg string = "图片文字识别失败" + PicRecognitionFailCode int = -400450 ) diff --git a/GinSkeleton/app/global/my_errors/my_errors.go b/GinSkeleton/app/global/my_errors/my_errors.go index 43289bf..5a76ab1 100644 --- a/GinSkeleton/app/global/my_errors/my_errors.go +++ b/GinSkeleton/app/global/my_errors/my_errors.go @@ -67,4 +67,9 @@ const ( ErrorCasbinCreateAdaptFail string = "casbin NewAdapterByDBUseTableName 发生错误:" ErrorCasbinCreateEnforcerFail string = "casbin NewEnforcer 发生错误:" ErrorCasbinNewModelFromStringFail string = "NewModelFromString 调用时出错:" + + // baiduCE 访问出现的错误 + ErrorBaiduCEGetTokenFail string = "访问百度智能云时,获得access_token 出现问题:" + ErrorBaiduCEUseOCRFail string = "使用百度智能云OCR接口失败:" + ErrorBaiduCEPostFail string = "使用百度智能云接口失败(POST通用):" ) diff --git a/GinSkeleton/app/http/controller/web/ai_recognition_controller.go b/GinSkeleton/app/http/controller/web/ai_recognition_controller.go new file mode 100644 index 0000000..52a58c4 --- /dev/null +++ b/GinSkeleton/app/http/controller/web/ai_recognition_controller.go @@ -0,0 +1,23 @@ +package web + +import ( + "goskeleton/app/global/consts" + "goskeleton/app/service/ai_model_cli" + "goskeleton/app/utils/response" + + "github.com/gin-gonic/gin" +) + +type AiRecognition struct { +} + +// Ai 模型识别模块,与ai相关的识别功能,包括:图片文字识别、语音识别 +// +// 图片文字识别 +func (u *AiRecognition) PicRecognition(context *gin.Context) { + if r, recogWords := ai_model_cli.RequestOCR(context); r { + response.Success(context, consts.CurdStatusOkMsg, recogWords) + } else { + response.Fail(context, consts.PicRecognitionFailCode, consts.PicRecognitionFailMsg, "") + } +} diff --git a/GinSkeleton/app/http/validator/common/register_validator/web_register_validator.go b/GinSkeleton/app/http/validator/common/register_validator/web_register_validator.go index d08d28f..c6b8ebb 100644 --- a/GinSkeleton/app/http/validator/common/register_validator/web_register_validator.go +++ b/GinSkeleton/app/http/validator/common/register_validator/web_register_validator.go @@ -5,6 +5,7 @@ import ( "goskeleton/app/global/consts" "goskeleton/app/http/validator/common/upload_files" "goskeleton/app/http/validator/common/websocket" + "goskeleton/app/http/validator/web/ai_recognition" "goskeleton/app/http/validator/web/users" ) @@ -46,4 +47,8 @@ func WebRegisterValidator() { // Websocket 连接验证器 key = consts.ValidatorPrefix + "WebsocketConnect" containers.Set(key, websocket.Connect{}) + + // ai 识别功能相关 + key = consts.ValidatorPrefix + "PicRecognition" + containers.Set(key, ai_recognition.PicRecognition{}) } diff --git a/GinSkeleton/app/http/validator/web/ai_recognition/pic_recognition.go b/GinSkeleton/app/http/validator/web/ai_recognition/pic_recognition.go new file mode 100644 index 0000000..be7442e --- /dev/null +++ b/GinSkeleton/app/http/validator/web/ai_recognition/pic_recognition.go @@ -0,0 +1,61 @@ +package ai_recognition + +import ( + "goskeleton/app/global/consts" + "goskeleton/app/http/controller/web" + "goskeleton/app/http/validator/core/data_transfer" + "goskeleton/app/utils/response" + + "github.com/gin-gonic/gin" +) + +type PicRecognition struct { + Pic string `form:"pic" json:"pic" binding:"required"` // 必填、对于文本,表示它的长度>=1 +} + +func (p PicRecognition) CheckParams(context *gin.Context) { + if err := context.ShouldBind(&p); err != nil { + // 将表单参数验证器出现的错误直接交给错误翻译器统一处理即可 + response.ValidatorError(context, err) + return + } + // 该函数主要是将本结构体的字段(成员)按照 consts.ValidatorPrefix+ json标签对应的 键 => 值 形式绑定在上下文,便于下一步(控制器)可以直接通过 context.Get(键) 获取相关值 + extraAddBindDataContext := data_transfer.DataAddContext(p, consts.ValidatorPrefix, context) + if extraAddBindDataContext == nil { + response.ErrorSystem(context, "PicRecognition表单参数验证器json化失败", "") + return + } + // TODO:linlnf + // pic := p.Pic + // // 先进行 URL 解码 + // decodedPic, err := url.QueryUnescape(pic) + // if err != nil { + // response.ErrorSystem(context, "PicRecognition表单参数验证器URL解码失败", "") + // return + // } + // // 再进行 Base64 解码 + // decodedData, err := base64.StdEncoding.DecodeString(decodedPic) + // if err != nil { + // response.ErrorSystem(context, "PicRecognition表单参数验证器Base64解码失败", "") + // return + // } + // // 检查大小不超过 10M(10 * 1024 * 1024 字节) + // if len(decodedData) > 10*1024*1024 { + // response.ErrorSystem(context, "PicRecognition表单参数验证器图片大小超过 10M", "") + // return + // } + // // 将字节数据转换为图像以检查格式和尺寸 + // img, _, err := image.Decode(bytes.NewReader(decodedData)) + // if err != nil { + // return + // } + // bounds := img.Bounds() + // minSide := math.Min(float64(bounds.Dx()), float64(bounds.Dy())) + // maxSide := math.Max(float64(bounds.Dx()), float64(bounds.Dy())) + // // 最短边至少 15px,最长边最大 8192px + // if minSide < 15 || maxSide > 8192 { + // response.ErrorSystem(context, "PicRecognition表单参数验证器图片尺寸过大或过小", "") + // return + // } + (&web.AiRecognition{}).PicRecognition(extraAddBindDataContext) +} diff --git a/GinSkeleton/app/service/ai_model_cli/ocr_cli.go b/GinSkeleton/app/service/ai_model_cli/ocr_cli.go new file mode 100644 index 0000000..8a2192f --- /dev/null +++ b/GinSkeleton/app/service/ai_model_cli/ocr_cli.go @@ -0,0 +1,76 @@ +package ai_model_cli + +import ( + "goskeleton/app/global/my_errors" + "goskeleton/app/global/variable" + "goskeleton/app/utils/baidubce" + "goskeleton/app/utils/files" + "net/http" + "net/url" + "strings" + + "github.com/gin-gonic/gin" +) + +/** + * 向百度智能云ocr(基础精准版)发送消息, 获得识别消息 + * @return 识别出的信息的内容 + */ +func RequestOCR(context *gin.Context) (r bool, c interface{}) { + path := "https://aip.baidubce.com/rest/2.0/ocr/v1/accurate_basic?access_token=" + baidubce.GetAccessToken() + // 获得图片数据,需要 base64 和urlencode处理 + // linlnf: test + content, _ := files.GetFileBase64("storage/app/test/文字识别.png") + // 组装 body 数据,参数在该网址:https://cloud.baidu.com/doc/OCR/s/1k3h7y3db + data := make(url.Values) + data.Set("detect_direction", "false") + data.Set("paragraph", "false") + data.Set("probability", "false") + data.Set("multidirectional_recognize", "false") + data.Set("image", content) + payload := strings.NewReader(data.Encode()) + client := &http.Client{} + // 请求数据 + req, err := http.NewRequest("POST", path, payload) + if err != nil { + variable.ZapLog.Error(my_errors.ErrorBaiduCEUseOCRFail + err.Error()) + return false, nil + } + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + req.Header.Add("Accept", "application/json") + res, err := client.Do(req) + + if err != nil { + variable.ZapLog.Error(my_errors.ErrorBaiduCEUseOCRFail + err.Error()) + return false, nil + } + defer res.Body.Close() + //从res中提取识别出的字符信息 + mbody, err := baidubce.DecodeResBody(res, "ocr") + if err != nil { + variable.ZapLog.Error(my_errors.ErrorBaiduCEUseOCRFail + err.Error()) + return false, nil + } + return true, gin.H{ + "words": decodeBody2Str(mbody), + } +} + +/* + * 将返回的内容中的所有识别出的文字信息进行组合 + * @return 识别出的信息的内容字符串 + */ +func decodeBody2Str(mbody map[string]interface{}) (str string) { + words, ok := mbody["words_result"] + if ok { + sliceWords, ok := words.([]interface{}) + if ok { + // 遍历切片 + for _, word := range sliceWords { + str += word.(map[string]interface{})["words"].(string) + "\n" + } + } + return str + } + return "" +} diff --git a/GinSkeleton/app/utils/baidubce/baidubce.go b/GinSkeleton/app/utils/baidubce/baidubce.go new file mode 100644 index 0000000..d8221c5 --- /dev/null +++ b/GinSkeleton/app/utils/baidubce/baidubce.go @@ -0,0 +1,61 @@ +package baidubce + +import ( + "encoding/json" + "errors" + "fmt" + "goskeleton/app/global/my_errors" + "goskeleton/app/global/variable" + "io" + "net/http" + "strings" +) + +/** + * 使用 AK,SK 生成鉴权签名(Access Token) + * @return string 鉴权签名信息(Access Token) + */ +func GetAccessToken() string { + url := "https://aip.baidubce.com/oauth/2.0/token" + API_KEY := variable.ConfigYml.GetString("BaiduCE.ApiKey") + SECRET_KEY := variable.ConfigYml.GetString("BaiduCE.SecretKey") + postData := fmt.Sprintf("grant_type=client_credentials&client_id=%s&client_secret=%s", API_KEY, SECRET_KEY) + resp, err := http.Post(url, "application/x-www-form-urlencoded", strings.NewReader(postData)) + if err != nil { + variable.ZapLog.Error(my_errors.ErrorBaiduCEGetTokenFail + err.Error()) + return "" + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + variable.ZapLog.Error(my_errors.ErrorBaiduCEGetTokenFail + err.Error()) + return "" + } + accessTokenObj := map[string]any{} + _ = json.Unmarshal([]byte(body), &accessTokenObj) + return accessTokenObj["access_token"].(string) +} + +func DecodeResBody(res *http.Response, flag string) (mbody map[string]interface{}, err error) { + body, err := io.ReadAll(res.Body) + if err != nil { + variable.ZapLog.Error(my_errors.ErrorBaiduCEPostFail + flag + ", " + err.Error()) + return nil, err + } + // json 格式的 body + var jbody interface{} + fmt.Println(string(body)) + err = json.Unmarshal(body, &jbody) + if err != nil { + variable.ZapLog.Error(my_errors.ErrorBaiduCEPostFail + flag + ", " + err.Error()) + return nil, err + } + mbody = jbody.(map[string]interface{}) + err_str, _ := mbody["error_msg"].(string) + if err_str != "" { + variable.ZapLog.Error(my_errors.ErrorBaiduCEPostFail + flag + ", " + err_str) + err = errors.New(err_str) + return nil, err + } + return mbody, err +} diff --git a/GinSkeleton/app/utils/files/baseInfo.go b/GinSkeleton/app/utils/files/baseInfo.go index 88249c5..c331b36 100644 --- a/GinSkeleton/app/utils/files/baseInfo.go +++ b/GinSkeleton/app/utils/files/baseInfo.go @@ -1,6 +1,7 @@ package files import ( + "encoding/base64" "goskeleton/app/global/my_errors" "goskeleton/app/global/variable" "mime/multipart" @@ -47,3 +48,12 @@ func GetFilesMimeByFp(fp multipart.File) string { return http.DetectContentType(buffer) } + +// 读取本地文件,进行Base64编码 +func GetFileBase64(filePath string) (string, error) { + data, err := os.ReadFile(filePath) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(data), nil +} diff --git a/GinSkeleton/config/config.yml b/GinSkeleton/config/config.yml index 4238184..f8ac485 100644 --- a/GinSkeleton/config/config.yml +++ b/GinSkeleton/config/config.yml @@ -144,3 +144,7 @@ Captcha: captchaValue: "captcha_value" #验证码值提交时的键名 length: 4 # 验证码生成时的长度 + +BaiduCE: + ApiKey: "AR1SUIjaKSsCcDjj11QzHDOc" # 生成鉴权签名时使用的 API_KEY + SecretKey: "zvEb5CzpuGCZNdQC1TPmDh3IOWn5aWDT" # 生成鉴权签名时使用的 SECRET_KEY diff --git a/GinSkeleton/routers/web.go b/GinSkeleton/routers/web.go index d2767de..a83f502 100644 --- a/GinSkeleton/routers/web.go +++ b/GinSkeleton/routers/web.go @@ -61,6 +61,13 @@ func InitWebRouter_Co() *gin.Engine { // 刷新token,当过期的token在允许失效的延长时间范围内,用旧token换取新token refreshToken.Use(authorization.RefreshTokenConditionCheck()).POST("refreshtoken", validatorFactory.Create(consts.ValidatorPrefix+"RefreshToken")) } + // TODO:linlnf + // 人工智能识别相关 + aiRecognition := backend.Group("ai_recognition/") + { + // 请求图片文字识别 + aiRecognition.POST("pic_recognition", validatorFactory.Create(consts.ValidatorPrefix+"PicRecognition")) + } // 【需要token】中间件验证的路由 backend.Use(authorization.CheckTokenAuth()) diff --git a/GinSkeleton/storage/app/test/文字识别.png b/GinSkeleton/storage/app/test/文字识别.png new file mode 100644 index 0000000..8e1bce2 Binary files /dev/null and b/GinSkeleton/storage/app/test/文字识别.png differ