Files
httprunner/hrp/pkg/uixt/service_vedem.go
2023-09-05 21:37:08 +08:00

580 lines
15 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package uixt
import (
"bytes"
"fmt"
"image"
"io"
"math"
"mime/multipart"
"net/http"
"regexp"
"strings"
"time"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/httprunner/httprunner/v4/hrp/internal/builtin"
"github.com/httprunner/httprunner/v4/hrp/internal/code"
"github.com/httprunner/httprunner/v4/hrp/internal/env"
"github.com/httprunner/httprunner/v4/hrp/internal/json"
)
var client = &http.Client{
Timeout: time.Second * 10,
}
type OCRResult struct {
Text string `json:"text"`
Points []PointF `json:"points"`
}
type OCRResults []OCRResult
func (o OCRResults) ToOCRTexts() (ocrTexts OCRTexts) {
for _, ocrResult := range o {
rect := image.Rectangle{
// ocrResult.Points 顺序:左上 -> 右上 -> 右下 -> 左下
Min: image.Point{
X: int(ocrResult.Points[0].X),
Y: int(ocrResult.Points[0].Y),
},
Max: image.Point{
X: int(ocrResult.Points[2].X),
Y: int(ocrResult.Points[2].Y),
},
}
ocrText := OCRText{
Text: ocrResult.Text,
Rect: rect,
}
ocrTexts = append(ocrTexts, ocrText)
}
return
}
type ImageResult struct {
URL string `json:"url,omitempty"` // image uploaded url
OCRResult OCRResults `json:"ocrResult,omitempty"` // OCR texts
// NoLive非直播间
// Shop电商
// LifeService生活服务
// Show秀场
// Game游戏
// People多人
// PKPK
// Media媒体
// Chat语音
// Event赛事
LiveType string `json:"liveType,omitempty"` // 直播间类型
UIResult UIResultMap `json:"uiResult,omitempty"` // 图标检测
CPResult *ClosePopupsResult `json:"closeResult,omitempty"` // 弹窗按钮检测
}
type APIResponseImage struct {
Code int `json:"code"`
Message string `json:"message"`
Result ImageResult `json:"result"`
}
type OCRText struct {
Text string
Rect image.Rectangle
}
func (t OCRText) Center() PointF {
return getRectangleCenterPoint(t.Rect)
}
type OCRTexts []OCRText
func (t OCRTexts) texts() (texts []string) {
for _, text := range t {
texts = append(texts, text.Text)
}
return texts
}
func (t OCRTexts) FilterScope(scope AbsScope) (results OCRTexts) {
for _, ocrText := range t {
rect := ocrText.Rect
// check if text in scope
if len(scope) == 4 {
if rect.Min.X < scope[0] ||
rect.Min.Y < scope[1] ||
rect.Max.X > scope[2] ||
rect.Max.Y > scope[3] {
// not in scope
continue
}
}
results = append(results, ocrText)
}
return
}
func (t OCRTexts) FindText(text string, options ...ActionOption) (result OCRText, err error) {
actionOptions := NewActionOptions(options...)
var results []OCRText
for _, ocrText := range t.FilterScope(actionOptions.AbsScope) {
if actionOptions.Regex {
// regex on, check if match regex
if !regexp.MustCompile(text).MatchString(ocrText.Text) {
continue
}
} else {
// regex off, check if match exactly
if ocrText.Text != text {
continue
}
}
results = append(results, ocrText)
// return the first one matched exactly when index not specified
if ocrText.Text == text && actionOptions.Index == 0 {
return ocrText, nil
}
}
if len(results) == 0 {
return OCRText{}, errors.Wrap(code.CVResultNotFoundError,
fmt.Sprintf("text %s not found in %v", text, t.texts()))
}
// get index
idx := actionOptions.Index
if idx < 0 {
idx = len(results) + idx
}
// index out of range
if idx >= len(results) || idx < 0 {
return OCRText{}, errors.Wrap(code.CVResultNotFoundError,
fmt.Sprintf("text %s found %d, index %d out of range", text, len(results), idx))
}
return results[idx], nil
}
func (t OCRTexts) FindTexts(texts []string, options ...ActionOption) (results OCRTexts, err error) {
actionOptions := NewActionOptions(options...)
for _, text := range texts {
ocrText, err := t.FindText(text, options...)
if err != nil {
continue
}
results = append(results, ocrText)
}
if len(results) == len(texts) {
return results, nil
}
if actionOptions.MatchOne && len(results) > 0 {
return results, nil
}
return nil, errors.Wrap(code.CVResultNotFoundError,
fmt.Sprintf("texts %s not found in %v", texts, t.texts()))
}
func newVEDEMImageService() (*veDEMImageService, error) {
if err := checkEnv(); err != nil {
return nil, err
}
return &veDEMImageService{}, nil
}
// veDEMImageService implements IImageService interface
// actions:
//
// ocr - get ocr texts
// upload - get image uploaded url
// liveType - get live type
// popup - get popup windows
// close - get close popup
// ui - get ui position by type(s)
type veDEMImageService struct{}
func (s *veDEMImageService) GetImage(imageBuf *bytes.Buffer, options ...ActionOption) (imageResult *ImageResult, err error) {
actionOptions := NewActionOptions(options...)
screenshotActions := actionOptions.screenshotActions()
if len(screenshotActions) == 0 {
// skip
return nil, nil
}
bodyBuf := &bytes.Buffer{}
bodyWriter := multipart.NewWriter(bodyBuf)
for _, action := range screenshotActions {
bodyWriter.WriteField("actions", action)
}
for _, uiType := range actionOptions.ScreenShotWithUITypes {
bodyWriter.WriteField("uiTypes", uiType)
}
bodyWriter.WriteField("ocrCluster", "highPrecision")
formWriter, err := bodyWriter.CreateFormFile("image", "screenshot.png")
if err != nil {
err = errors.Wrap(code.CVRequestError,
fmt.Sprintf("create form file error: %v", err))
return
}
size, err := formWriter.Write(imageBuf.Bytes())
if err != nil {
err = errors.Wrap(code.CVRequestError,
fmt.Sprintf("write form error: %v", err))
return
}
err = bodyWriter.Close()
if err != nil {
err = errors.Wrap(code.CVRequestError,
fmt.Sprintf("close body writer error: %v", err))
return
}
var req *http.Request
var resp *http.Response
// retry 3 times
for i := 1; i <= 3; i++ {
copiedBodyBuf := &bytes.Buffer{}
if _, err := copiedBodyBuf.Write(bodyBuf.Bytes()); err != nil {
log.Error().Err(err).Msg("copy screenshot buffer failed")
continue
}
req, err = http.NewRequest("POST", env.VEDEM_IMAGE_URL, copiedBodyBuf)
if err != nil {
err = errors.Wrap(code.CVRequestError,
fmt.Sprintf("construct request error: %v", err))
return
}
signToken := "UNSIGNED-PAYLOAD"
token := builtin.Sign("auth-v2", env.VEDEM_IMAGE_AK, env.VEDEM_IMAGE_SK, []byte(signToken))
req.Header.Add("Agw-Auth", token)
req.Header.Add("Agw-Auth-Content", signToken)
req.Header.Add("Content-Type", bodyWriter.FormDataContentType())
start := time.Now()
resp, err = client.Do(req)
elapsed := time.Since(start)
if err != nil {
log.Error().Err(err).
Int("imageBufSize", size).
Msgf("request veDEM OCR service error, retry %d", i)
continue
}
logID := getLogID(resp.Header)
statusCode := resp.StatusCode
if statusCode != http.StatusOK {
log.Error().
Str("X-TT-LOGID", logID).
Int("imageBufSize", size).
Int("statusCode", statusCode).
Msgf("request veDEM OCR service failed, retry %d", i)
time.Sleep(1 * time.Second)
continue
}
log.Debug().
Str("X-TT-LOGID", logID).
Int("image_bytes", size).
Int64("elapsed(ms)", elapsed.Milliseconds()).
Msg("request OCR service success")
break
}
if resp == nil {
err = code.CVServiceConnectionError
return
}
defer resp.Body.Close()
results, err := io.ReadAll(resp.Body)
if err != nil {
err = errors.Wrap(code.CVResponseError,
fmt.Sprintf("read response body error: %v", err))
return
}
if resp.StatusCode != http.StatusOK {
err = errors.Wrap(code.CVResponseError,
fmt.Sprintf("unexpected response status code: %d, results: %v",
resp.StatusCode, string(results)))
return
}
var imageResponse APIResponseImage
err = json.Unmarshal(results, &imageResponse)
if err != nil {
log.Error().Err(err).
Str("response", string(results)).
Msg("json unmarshal veDEM image response body failed")
err = errors.Wrap(code.CVResponseError,
"json unmarshal veDEM image response body error")
return
}
if imageResponse.Code != 0 {
log.Error().
Int("code", imageResponse.Code).
Str("message", imageResponse.Message).
Msg("request veDEM OCR service failed")
}
imageResult = &imageResponse.Result
log.Debug().Interface("imageResult", imageResult).Msg("get image data by veDEM")
return imageResult, nil
}
func checkEnv() error {
if env.VEDEM_IMAGE_URL == "" {
return errors.Wrap(code.CVEnvMissedError, "VEDEM_IMAGE_URL missed")
}
log.Info().Str("VEDEM_IMAGE_URL", env.VEDEM_IMAGE_URL).Msg("get env")
if env.VEDEM_IMAGE_AK == "" {
return errors.Wrap(code.CVEnvMissedError, "VEDEM_IMAGE_AK missed")
}
if env.VEDEM_IMAGE_SK == "" {
return errors.Wrap(code.CVEnvMissedError, "VEDEM_IMAGE_SK missed")
}
return nil
}
func getLogID(header http.Header) string {
if len(header) == 0 {
return ""
}
logID, ok := header["X-Tt-Logid"]
if !ok || len(logID) == 0 {
return ""
}
return logID[0]
}
type IImageService interface {
// GetImage returns image result including ocr texts, uploaded image url, etc
GetImage(imageBuf *bytes.Buffer, options ...ActionOption) (imageResult *ImageResult, err error)
}
// GetScreenResult takes a screenshot, returns the image recognization result
func (dExt *DriverExt) GetScreenResult(options ...ActionOption) (screenResult *ScreenResult, err error) {
startTime := time.Now()
fileName := builtin.GenNameWithTimestamp("%d_screenshot")
actionOptions := NewActionOptions(options...)
screenshotActions := actionOptions.screenshotActions()
if len(screenshotActions) != 0 {
fileName = builtin.GenNameWithTimestamp("%d_" + strings.Join(screenshotActions, "_"))
}
bufSource, imagePath, err := dExt.takeScreenShot(fileName)
if err != nil {
return
}
screenshotTakeElapsed := time.Since(startTime).Milliseconds()
screenResult = &ScreenResult{
bufSource: bufSource,
imagePath: imagePath,
Tags: nil,
Resolution: dExt.windowSize,
ScreenshotTakeElapsed: screenshotTakeElapsed,
}
imageResult, err := dExt.ImageService.GetImage(bufSource, options...)
if err != nil {
log.Error().Err(err).Msg("GetImage from ImageService failed")
return nil, err
}
if imageResult != nil {
screenResult.imageResult = imageResult
screenResult.ScreenshotCVElapsed = time.Since(startTime).Milliseconds() - screenshotTakeElapsed
screenResult.Texts = imageResult.OCRResult.ToOCRTexts()
screenResult.UploadedURL = imageResult.URL
screenResult.Icons = imageResult.UIResult
if actionOptions.ScreenShotWithClosePopups {
screenResult.Popup = &PopupInfo{
Type: imageResult.CPResult.Type,
Text: imageResult.CPResult.Text,
PicName: imagePath,
PicURL: imageResult.URL,
PopupArea: imageResult.CPResult.PopupArea,
CloseArea: imageResult.CPResult.CloseArea,
}
}
}
dExt.cacheStepData.screenResults[time.Now().String()] = screenResult
log.Debug().
Str("imagePath", imagePath).
Str("imageUrl", screenResult.UploadedURL).
Int64("screenshot_take_elapsed(ms)", screenResult.ScreenshotTakeElapsed).
Int64("screenshot_cv_elapsed(ms)", screenResult.ScreenshotCVElapsed).
Msg("log screenshot")
return screenResult, nil
}
func (dExt *DriverExt) GetScreenTexts() (ocrTexts OCRTexts, err error) {
screenResult, err := dExt.GetScreenResult(
WithScreenShotOCR(true), WithScreenShotUpload(true))
if err != nil {
return
}
return screenResult.Texts, nil
}
func (dExt *DriverExt) FindScreenText(text string, options ...ActionOption) (point PointF, err error) {
ocrTexts, err := dExt.GetScreenTexts()
if err != nil {
return
}
result, err := ocrTexts.FindText(text, dExt.ParseActionOptions(options...)...)
if err != nil {
log.Warn().Msgf("FindText failed: %s", err.Error())
return
}
point = result.Center()
log.Info().Str("text", text).
Interface("point", point).Msgf("FindScreenText success")
return
}
func getRectangleCenterPoint(rect image.Rectangle) (point PointF) {
x, y := float64(rect.Min.X), float64(rect.Min.Y)
width, height := float64(rect.Dx()), float64(rect.Dy())
point = PointF{
X: x + width*0.5,
Y: y + height*0.5,
}
return point
}
type Box struct {
Point PointF `json:"point"`
Width float64 `json:"width"`
Height float64 `json:"height"`
}
func (box Box) IsEmpty() bool {
return builtin.IsZeroFloat64(box.Width) && builtin.IsZeroFloat64(box.Height)
}
func (box Box) IsIdentical(box2 Box) bool {
return box.Point.IsIdentical(box2.Point) &&
builtin.IsZeroFloat64(math.Abs(box.Width-box2.Width)) &&
builtin.IsZeroFloat64(math.Abs(box.Height-box2.Height))
}
func (box Box) Center() PointF {
return PointF{
X: box.Point.X + box.Width*0.5,
Y: box.Point.Y + box.Height*0.5,
}
}
type UIResult struct {
Box
}
type UIResults []UIResult
func (u UIResults) FilterScope(scope AbsScope) (results UIResults) {
for _, uiResult := range u {
rect := image.Rectangle{
Min: image.Point{
X: int(uiResult.Point.X),
Y: int(uiResult.Point.Y),
},
Max: image.Point{
X: int(uiResult.Point.X + uiResult.Width),
Y: int(uiResult.Point.Y + uiResult.Height),
},
}
// check if ui result in scope
if len(scope) == 4 {
if rect.Min.X < scope[0] ||
rect.Min.Y < scope[1] ||
rect.Max.X > scope[2] ||
rect.Max.Y > scope[3] {
// not in scope
continue
}
}
results = append(results, uiResult)
}
return
}
type UIResultMap map[string]UIResults
// FilterUIResults filters ui icons, the former the uiTypes, the higher the priority
func (u UIResultMap) FilterUIResults(uiTypes []string) (uiResults UIResults, err error) {
var ok bool
for _, uiType := range uiTypes {
uiResults, ok = u[uiType]
if ok && len(uiResults) != 0 {
return
}
}
err = errors.Errorf("UI types %v not detected", uiTypes)
return
}
func (u UIResults) GetUIResult(options ...ActionOption) (UIResult, error) {
actionOptions := NewActionOptions(options...)
uiResults := u.FilterScope(actionOptions.AbsScope)
if len(uiResults) == 0 {
return UIResult{}, errors.Wrap(code.CVResultNotFoundError,
"ui types not found in scope")
}
// get index
idx := actionOptions.Index
if idx < 0 {
idx = len(uiResults) + idx
}
// index out of range
if idx >= len(uiResults) || idx < 0 {
return UIResult{}, errors.Wrap(code.CVResultNotFoundError,
fmt.Sprintf("ui types index %d out of range", idx))
}
return uiResults[idx], nil
}
func (dExt *DriverExt) FindUIResult(options ...ActionOption) (point PointF, err error) {
actionOptions := NewActionOptions(options...)
screenResult, err := dExt.GetScreenResult(options...)
if err != nil {
return
}
uiResults, err := screenResult.Icons.FilterUIResults(actionOptions.ScreenShotWithUITypes)
if err != nil {
return
}
uiResult, err := uiResults.GetUIResult(dExt.ParseActionOptions(options...)...)
point = uiResult.Center()
log.Info().Interface("text", actionOptions.ScreenShotWithUITypes).
Interface("point", point).Msg("FindUIResult success")
return
}