Golang实践:讯飞云人脸比对demo实现 | 豆包MarsCode AI 刷题

73 阅读5分钟

讯飞云人脸比对开发准备

  1. 到官网讯飞开放平台-以语音交互为核心的人工智能开放平台申请账号
  2. 申请人脸比对API服务接口认证
  3. 根据人脸比对 API 文档 | 讯飞开放平台文档中心进行开发

讯飞云人脸比对流程

  1. 向讯飞云人脸比对服务发起请求
  2. 接收并解析讯飞云人脸比对服务响应
  3. 获取比对信息

发起请求阶段

请求流程
  1. 进行鉴权认证
  2. 填入相关信息
  3. 发送请求
请求实现

这里我打算创建一个fr-cli来做请求的中间持久层,请求相关的通用配置只配置一次,只在请求的时候把准备好的数据经过鉴权认证后发送。

type FRClient struct {
        app_id     string
        api_key    string
        api_secret string

        fr_request *FRRequest
}

type FRClientOption func(*FRClient)

func WithClientInfo(app_id string, api_key string, api_secret string) FRClientOption {
        return func(fr_client *FRClient) {
                fr_client.app_id = app_id
                fr_client.api_key = api_key
                fr_client.api_secret = api_secret
        }
}

func NewFRClient(options ...FRClientOption) *FRClient {
        fr_client := &FRClient{}
        for _, option := range options {
                option(fr_client)
        }
        fr_client.fr_request = NewFRRequest(WithFRRequestAppID(fr_client.app_id))
        return fr_client
}

const (
        FIRST_INPUT = 1
        LAST_INPUT  = 2
)

func (tho *FRClient) AddInput(input_option uint, encoding string, image []byte) error {
        err := tho.fr_request.AddInput(input_option, encoding, image)
        if err != nil {
                return err
        }
        return nil
}

func (tho *FRClient) nowRFC1123() string {
        return time.Now().UTC().Format(time.RFC1123)
}

func (tho *FRClient) getAuthenticationParameters(date string) (authorization string, err error) {
        host := "api.xf-yun.com"
        request_line := "POST /v1/private/s67c9c78c HTTP/1.1"

        // 生成signature的原始字段(signature_origin)
        signature_origin := fmt.Sprintf("host: %s\ndate: %s\n%s", host, date, request_line)

        // 使用hmac-sha256算法结合apiSecret对signature_origin签名,获得签名后的摘要signature_sha
        hmac_sha256 := hmac.New(sha256.New, []byte(tho.api_secret))
        _, err = hmac_sha256.Write([]byte(signature_origin))
        if err != nil {
                return "", err
        }
        signature_sha := hmac_sha256.Sum(nil)

        // 使用base64编码对signature_sha进行编码获得最终的signature
        signature := base64.StdEncoding.EncodeToString(signature_sha)

        // 生成authorization base64编码前(authorization_origin)的字符串
        authorization_origin := fmt.Sprintf("api_key=\"%s\",algorithm=\"hmac-sha256\",headers=\"host date request-line\",signature=\"%s\"", tho.api_key, signature)

        // 对authorization_origin进行base64编码获得最终的authorization参数
        authorization = base64.StdEncoding.EncodeToString([]byte(authorization_origin))
        return authorization, nil
}

func (tho *FRClient) getRequestURL(authorization string, date string) string {
        return fmt.Sprintf("https://api.xf-yun.com/v1/private/s67c9c78c?authorization=%s&host=api.xf-yun.com&date=%s", authorization, url.QueryEscape(date))
}

func (tho *FRClient) newHasSuffixsFinder(suffixs ...string) func(string) (bool, string) {
        return func(s string) (bool, string) {
                has_suffix := false
                for _, suffix := range suffixs {
                        if strings.HasSuffix(s, suffix) {
                                has_suffix = true
                                return has_suffix, suffix
                        }
                }
                return has_suffix, ""
        }
}

func (tho *FRClient) ReadLocalImage(image_path string) (encoding string, image []byte, err error) {
        has_suffixs_finder := tho.newHasSuffixsFinder(".jpg", ".jpeg", ".png", ".bmp")
        has_suffix, encoding := has_suffixs_finder(image_path)
        if !has_suffix {
                return "", nil, errors.New("face_recognition: The target file is not in jpg/png/bmp format")
        }

        image_file, err := os.OpenFile(image_path, os.O_RDONLY, 0666)
        if err != nil {
                return "", nil, err
        }
        defer image_file.Close()

        image, err = io.ReadAll(image_file)
        if err != nil {
                return "", nil, err
        }

        return encoding[1:], image, nil
}

func (tho *FRClient) Do() (*FRResponseText, error) {
        date := tho.nowRFC1123()

        authorization, err := tho.getAuthenticationParameters(date)
        if err != nil {
                return nil, err
        }

        request_url := tho.getRequestURL(authorization, date)

        json_data, err := json.Marshal(tho.fr_request)
        if err != nil {
                return nil, err
        }
        request, err := http.NewRequest("POST", request_url, bytes.NewBuffer(json_data))
        if err != nil {
                return nil, err
        }

        request.Header.Set("content-type", "application/json")
        request.Header.Set("host", "api.xf-yun.com")
        request.Header.Set("app_id", tho.app_id)

        http_client := &http.Client{}

        response, err := http_client.Do(request)
        if err != nil {
                return nil, err
        }
        defer response.Body.Close()

        json_data, err = io.ReadAll(response.Body)
        if err != nil {
                return nil, err
        }

        fr_response := &FRResponse{}
        err = json.Unmarshal(json_data, fr_response)
        if err != nil {
                return nil, err
        }

        return fr_response.GetResponseText(), nil
}

在上层结构已经完成了所有配置后,我们的请求和响应结构只需要专注于生成和解析相关正确的结构即可。请求结构只需要把图片数据正确添加并编码就好了,所有所需要的数据只需要等着fr-cli喂过来就行。

简单来说就是,FRRequest只需要把数据正确添加编码就好了,而FRClient需要考虑的就多了。

type FRRequest struct {
        Header struct {
                App_id string `json:"app_id"`
                Status int    `json:"status"`
        } `json:"header"`
        Parameter struct {
                S67c9c78c struct {
                        Service_kind        string `json:"service_kind"`
                        Face_compare_result struct {
                                Encoding string `json:"encoding"`
                                Compress string `json:"compress"`
                                Format   string `json:"format"`
                        } `json:"face_compare_result"`
                } `json:"s67c9c78c"`
        } `json:"parameter"`
        Payload struct {
                Input1 struct {
                        Encoding string `json:"encoding"`
                        Status   int    `json:"status"`
                        Image    string `json:"image"`
                } `json:"input1"`
                Input2 struct {
                        Encoding string `json:"encoding"`
                        Status   int    `json:"status"`
                        Image    string `json:"image"`
                } `json:"input2"`
        } `json:"payload"`
}

type FRRequestOption func(*FRRequest)

func WithFRRequestAppID(app_id string) FRRequestOption {
        return func(frr *FRRequest) {
                frr.Header.App_id = app_id
        }
}

func WithFRRequestInput(input_option uint, encoding string, image []byte) (FRRequestOption, error) {
        if input_option > 2 {
                return nil, errors.New("xfyun_fr: The input options can only be 1 or 2")
        }
        if image == nil {
                return nil, errors.New("xfyun_fr: The image is empty")
        }
        base64_image := base64.StdEncoding.EncodeToString(image)
        if len(base64_image) > 4*1024*1024 {
                return nil, errors.New("xfyun_fr: The base64 image is larger than 4M")
        }

        return func(frr *FRRequest) {
                if input_option == 1 {
                        frr.Payload.Input1.Encoding = encoding
                        frr.Payload.Input1.Image = base64_image
                } else if input_option == 2 {
                        frr.Payload.Input2.Encoding = encoding
                        frr.Payload.Input2.Image = base64_image
                }
        }, nil
}

func NewFRRequest(options ...FRRequestOption) *FRRequest {
        fr_request := &FRRequest{}
        fr_request.Header.Status = 3
        fr_request.Parameter.S67c9c78c.Service_kind = "face_compare"
        fr_request.Parameter.S67c9c78c.Face_compare_result.Encoding = "utf8"
        fr_request.Parameter.S67c9c78c.Face_compare_result.Compress = "raw"
        fr_request.Parameter.S67c9c78c.Face_compare_result.Format = "json"
        fr_request.Payload.Input1.Status = 3
        fr_request.Payload.Input2.Status = 3

        for _, option := range options {
                option(fr_request)
        }

        return fr_request
}

func (tho *FRRequest) AddInput(input_option uint, encoding string, image []byte) error {
        if input_option > 2 {
                return errors.New("xfyun_fr: The input options can only be 1 or 2")
        }
        if image == nil {
                return errors.New("xfyun_fr: The image is empty")
        }
        base64_image := base64.StdEncoding.EncodeToString(image)
        if len(base64_image) > 4*1024*1024 {
                return errors.New("xfyun_fr: The base64 image is larger than 4M")
        }

        if input_option == 1 {
                tho.Payload.Input1.Encoding = encoding
                tho.Payload.Input1.Image = base64_image
        } else if input_option == 2 {
                tho.Payload.Input2.Encoding = encoding
                tho.Payload.Input2.Image = base64_image
        }

        return nil
}

请求的使用就很简单了。先通过NewFRClient获取一个cli,通过AddInput添加数据,然后通过Do发送请求即可。

client := NewFRClient(WithClientInfo("YOUR_APP_ID", "YOUR_API_KEY", "YOUR_API_SECRET"))
client.AddInput(FIRST_INPUT, "IMAGE_TYPE", []byte(""))
client.AddInput(LAST_INPUT, "IMAGE_TYPE", []byte(""))
resp, err := client.Do()

响应阶段

响应流程
  1. 解析响应
  2. 解码信息
  3. 解析解码结果
响应实现

由于fr-cli的存在,我们可以把接收的响应在fr-cli的方法内部进行解析和解码,最终只返回错误或比对结果。所以在cli的Do方法中,我们就将接收到的响应进行解析,然后将比对信息进行解码,最后返回给调用方分析结果。而为了更便捷的获取调用结果,我根据官方推荐的标准定义了比对阈值。

type FRResponseText struct {
        Ret   int     `json:"ret"`
        Score float64 `json:"score"`
}

func (tho *FRResponseText) IsSuccess() bool {
        return tho.Score > 0.67
}

type FRResponse struct {
        Header struct {
                Code    int    `json:"code"`
                Message string `json:"message"`
                Sid     string `json:"sid"`
        } `json:"header"`
        Payload struct {
                Face_compare_result struct {
                        Compress string `json:"compress"`
                        Encoding string `json:"encoding"`
                        Format   string `json:"format"`
                        Text     string `json:"text"`
                } `json:"face_compare_result"`
        } `json:"payload"`
}

type FRResponseOption func(*FRResponse)

func (tho *FRResponse) GetResponseText() *FRResponseText {
        base64_text := tho.Payload.Face_compare_result.Text
        json_text, err := base64.StdEncoding.DecodeString(base64_text)
        if err != nil {
                return nil
        }

        response_text := &FRResponseText{}
        err = json.Unmarshal(json_text, response_text)
        if err != nil {
                return nil
        }

        return response_text
}

总的来说,讯飞云人脸比对demo的实现是比较简单的,比较复杂的点在于结构的创建和鉴权认证的实现,像base64编解码方面,Golang都是有成熟的库可以调用的。