Explorar o código

增加aliyun实现ocr手写识别服务接口

luoshi hai 1 ano
pai
achega
b468f89324

+ 58 - 0
src/main/java/com/qmth/ops/biz/ai/client/OcrApiClient.java

@@ -0,0 +1,58 @@
+package com.qmth.ops.biz.ai.client;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.qmth.boot.core.rateLimit.service.RateLimiter;
+import com.qmth.boot.core.rateLimit.service.impl.MemoryRateLimiter;
+import com.qmth.ops.biz.ai.exception.OcrRateLimitExceeded;
+import okhttp3.*;
+
+import java.io.IOException;
+
+/**
+ * 大模型chat类接口基础实现
+ */
+public abstract class OcrApiClient {
+
+    private OkHttpClient client;
+
+    private OcrApiConfig config;
+
+    private ObjectMapper mapper;
+
+    private RateLimiter queryRateLimiter;
+
+    public OcrApiClient(OcrApiConfig config) {
+        this.client = new OkHttpClient.Builder().connectionPool(new ConnectionPool()).build();
+        this.mapper = new ObjectMapper();
+        this.config = config;
+        if (config.getQps() > 0) {
+            this.queryRateLimiter = new MemoryRateLimiter(config.getQps(), 1000);
+        }
+    }
+
+    protected OcrApiConfig getConfig() {
+        return config;
+    }
+
+    protected abstract String buildUrl() throws Exception;
+
+    protected abstract String buildResult(byte[] data, ObjectMapper mapper) throws IOException;
+
+    protected abstract String handleError(byte[] data, int statusCode, ObjectMapper mapper);
+
+    public String call(byte[] image) throws Exception {
+        if (queryRateLimiter != null && !queryRateLimiter.acquire()) {
+            throw new OcrRateLimitExceeded(config.getQps());
+        }
+        RequestBody body = RequestBody.create(MediaType.parse("application/octet-stream"), image);
+        Request httpRequest = new Request.Builder().url(buildUrl()).post(body).build();
+        Response response = client.newCall(httpRequest).execute();
+        byte[] data = response.body() != null ? response.body().bytes() : null;
+        if (response.isSuccessful()) {
+            return buildResult(data, mapper);
+        } else {
+            return handleError(data, response.code(), mapper);
+        }
+    }
+
+}

+ 44 - 0
src/main/java/com/qmth/ops/biz/ai/client/OcrApiConfig.java

@@ -0,0 +1,44 @@
+package com.qmth.ops.biz.ai.client;
+
+public class OcrApiConfig {
+
+    private String url;
+
+    private String key;
+
+    private String secret;
+
+    private int qps;
+
+    public String getUrl() {
+        return url;
+    }
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+    public void setKey(String key) {
+        this.key = key;
+    }
+
+    public String getSecret() {
+        return secret;
+    }
+
+    public void setSecret(String secret) {
+        this.secret = secret;
+    }
+
+    public int getQps() {
+        return qps;
+    }
+
+    public void setQps(int qps) {
+        this.qps = qps;
+    }
+}

+ 171 - 0
src/main/java/com/qmth/ops/biz/ai/client/aliyun/AliyunOcrClient.java

@@ -0,0 +1,171 @@
+package com.qmth.ops.biz.ai.client.aliyun;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.qmth.boot.core.exception.ParameterException;
+import com.qmth.boot.core.exception.ReentrantException;
+import com.qmth.boot.core.exception.StatusException;
+import com.qmth.boot.core.exception.UnauthorizedException;
+import com.qmth.boot.tools.models.ByteArray;
+import com.qmth.boot.tools.uuid.FastUUID;
+import com.qmth.ops.biz.ai.client.OcrApiClient;
+import com.qmth.ops.biz.ai.client.OcrApiConfig;
+
+import javax.crypto.Mac;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import java.io.File;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URL;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.time.temporal.ChronoField;
+import java.util.*;
+
+public class AliyunOcrClient extends OcrApiClient {
+
+    private String action;
+
+    public AliyunOcrClient(OcrApiConfig config, String action) {
+        super(config);
+        this.action = action;
+    }
+
+    @Override
+    protected String buildResult(byte[] data, ObjectMapper mapper) throws IOException {
+        return mapper.readValue(data, AliyunOcrResult.class).getDataContent(mapper);
+    }
+
+    @Override
+    protected String handleError(byte[] data, int statusCode, ObjectMapper mapper) {
+        AliyunError error = null;
+        if (data != null) {
+            try {
+                error = mapper.readValue(data, AliyunError.class);
+            } catch (Exception e) {
+            }
+        }
+        switch (statusCode) {
+        case 400:
+        case 413:
+            throw new ParameterException(error != null ? error.getMessage() : "ocr request parameter error");
+        case 401:
+            throw new UnauthorizedException(error != null ? error.getMessage() : "ocr api unauthorized");
+        case 503:
+        case 504:
+            throw new ReentrantException(error != null ? error.getMessage() : "ocr api temporary faile");
+        default:
+            throw new StatusException(error != null ? error.getMessage() : "ocr api error");
+        }
+    }
+
+    private String percentEncode(String value) {
+        try {
+            return value != null ?
+                    URLEncoder.encode(value, StandardCharsets.UTF_8.name()).replace("+", "%20").replace("*", "%2A")
+                            .replace("%7E", "~") :
+                    null;
+        } catch (UnsupportedEncodingException ignore) {
+            return value;
+        }
+    }
+
+    private String urlEncode(String value) {
+        try {
+            return URLEncoder.encode(value, StandardCharsets.UTF_8.name());
+        } catch (UnsupportedEncodingException ignore) {
+            return value;
+        }
+    }
+
+    private String urlDecode(String value) {
+        try {
+            return URLDecoder.decode(value, StandardCharsets.UTF_8.name());
+        } catch (UnsupportedEncodingException ignore) {
+            return value;
+        }
+    }
+
+    public String getSignature(String url, String httpMethod) throws Exception {
+        // 解析url中的参数部分
+        URL u = new URL(url);
+        String query = u.getQuery();
+        // 获取范化的请求字符串
+        String canonicalString = Arrays.stream(query.split("&")).map(s -> s.split("="))
+                // 去掉不合法的空参数(例如: https://example?Url=&AccessKeyId=)
+                .filter(arr -> arr.length > 1)
+                // 根据请求参数的字典顺序排序
+                .sorted(Comparator.comparing(arr -> arr[0]))
+                // 按照 RFC3986 规则编码参数名称、参数值
+                .map(arr -> String.format("%s=%s", percentEncode(arr[0]), percentEncode(urlDecode(arr[1]))))
+                // 用"&"拼接起来编码后的参数
+                .reduce((s1, s2) -> s1 + "&" + s2).orElse("");
+        // 将规范化字符串拼接成待签名的字符串
+        String stringToSign = httpMethod + "&" + percentEncode("/") + "&" + percentEncode(canonicalString);
+        // 把 AccessKeySecret加上"&"构成 HMAC-SHA1 算法的key
+        String secret = getConfig().getSecret() + "&";
+        // HMAC-SHA1 编码后的bytes
+        SecretKey secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA1");
+        Mac mac = Mac.getInstance("HmacSHA1");
+        mac.init(secretKey);
+        byte[] hashBytes = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
+        // 按照 base64 编码规则生成最后的签名字符串
+        return Base64.getEncoder().encodeToString(hashBytes);
+    }
+
+    /**
+     * 获取公共请求参数。公共请求参数详见文档:https://help.aliyun.com/document_detail/145074.html
+     *
+     * @return 公共请求参数组成的字典
+     */
+    private Map<String, String> getCommonParameters(String action) {
+        return new HashMap<String, String>() {{
+            put("Action", action); // 调用的接口名称,此处以 RecognizeGeneral 为例
+            put("Version", "2021-07-07"); // API版本。OCR的固定值:2021-07-07
+            put("Format", "JSON"); // 指定接口返回数据的格式,可以选择 JSON 或者 XML
+            put("AccessKeyId", getConfig().getKey()); // 您的AccessKeyId
+            put("SignatureNonce", FastUUID.get()); // 签名唯一随机数,每次调用不可重复
+            put("Timestamp", Instant.now().with(ChronoField.NANO_OF_SECOND, 0)
+                    .toString()); // 需要Java8及以上版本。如果您需要使用更低版本Java,请更换获取时间戳的方法
+            put("SignatureMethod", "HMAC-SHA1"); // 签名方式。目前为固定值 HMAC-SHA1
+            put("SignatureVersion", "1.0"); // 签名方式。目前为固定值 1.0
+        }};
+    }
+
+    /**
+     * 识别本地文件代码示例。以 RecognizeGeneral 接口为例。
+     */
+    @Override
+    protected String buildUrl() throws Exception {
+        // 获取公共请求参数
+        Map<String, String> parametersMap = getCommonParameters(action);
+        // 初始化请求URL
+        StringBuilder urlBuilder = new StringBuilder(getConfig().getUrl());
+        urlBuilder.append("?");
+        // 把业务参数拼接到请求链接中
+        for (Map.Entry<String, String> entry : parametersMap.entrySet()) {
+            // entry.getValue() 可能出现"&"等符号。 需要encode
+            urlBuilder.append(String.format("%s=%s", entry.getKey(), urlEncode(entry.getValue()))).append('&');
+        }
+        // 去掉最后的"&"
+        String url = urlBuilder.substring(0, urlBuilder.length() - 1);
+        // 获取签名
+        String signature = getSignature(url, "POST");
+        // 按照 RFC3986 规则编码签名,并添加到最终的请求链接上
+        url += String.format("&Signature=%s", percentEncode(signature));
+        return url;
+    }
+
+    public static void main(String[] args) throws Exception {
+        OcrApiConfig config = new OcrApiConfig();
+        config.setUrl("https://ocr-api.cn-hangzhou.aliyuncs.com/");
+        config.setKey("LTAI5t6D5p62tDjYgwSz2mTR");
+        config.setSecret("twrXT7Dp1kG1bV5HZn6vgpoypu9PnZ");
+        config.setQps(0);
+        AliyunOcrClient client = new AliyunOcrClient(config, "RecognizeHandwriting");
+        System.out.println(client.call(ByteArray.fromFile(new File("/Users/luoshi/Downloads/cache/1-1.jpg")).value()));
+    }
+
+}

+ 19 - 0
src/main/java/com/qmth/ops/biz/ai/client/aliyun/AliyunOcrData.java

@@ -0,0 +1,19 @@
+package com.qmth.ops.biz.ai.client.aliyun;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class AliyunOcrData {
+
+    @JsonProperty("content")
+    private String content;
+
+    public String getContent() {
+        return content;
+    }
+
+    public void setContent(String content) {
+        this.content = content;
+    }
+}

+ 39 - 0
src/main/java/com/qmth/ops/biz/ai/client/aliyun/AliyunOcrResult.java

@@ -0,0 +1,39 @@
+package com.qmth.ops.biz.ai.client.aliyun;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class AliyunOcrResult {
+
+    @JsonProperty("RequestId")
+    private String requestId;
+
+    @JsonProperty("Data")
+    private String data;
+
+    public String getRequestId() {
+        return requestId;
+    }
+
+    public void setRequestId(String requestId) {
+        this.requestId = requestId;
+    }
+
+    public String getData() {
+        return data;
+    }
+
+    public void setData(String data) {
+        this.data = data;
+    }
+
+    public String getDataContent(ObjectMapper mapper) {
+        try {
+            return mapper.readValue(data, AliyunOcrData.class).getContent();
+        } catch (Exception e) {
+            return null;
+        }
+    }
+}

+ 12 - 0
src/main/java/com/qmth/ops/biz/ai/exception/OcrRateLimitExceeded.java

@@ -0,0 +1,12 @@
+package com.qmth.ops.biz.ai.exception;
+
+import com.qmth.boot.core.exception.ReentrantException;
+
+public class OcrRateLimitExceeded extends ReentrantException {
+
+    private static final long serialVersionUID = 5439278820114982090L;
+
+    public OcrRateLimitExceeded(int qps) {
+        super("OCR api rate limit exceeded, qps=" + qps);
+    }
+}