Langboat Intelligent Translation
Langboat Intelligent Translation
发送 POST 请求到:
澜舟科技机器翻译服务所提供的 API 接口均通过 HTTPS 进行通信,提供高安全性的通信通道。
按翻译成功的 待翻译文件内的 有效字符数统计用量。
请求头参数 | 是否必须 | 描述 |
---|---|---|
Accept | 是 | 固定值 application/json 。 |
Authorization | 是 | 用于验证请求合法性的认证信息,值为 AccessKey:signature格式。 |
Content-Type | 是 | 固定值 application/json 。 |
Content-MD5 | 是 | HTTP 协议消息体的 128-bit MD5 散列值转换成 Base64 编码的结果。 |
Date | 是 | 请求时间,GMT 格式,如: Wed, 20 Apr. 2022 17:01:00 GMT。 |
x-langboat-signature-nonce | 是 | 唯一随机数,用于防止网络重放攻击。在不同请求中要使用不同的随机数值。 |
x-langboat-signature-method | 是 | 签名方法,目前只支持 HMAC-SHA256 。 |
Content-MD5
Header计算示例:
{"sourceText": "Where there is a will, there is a way."}
de56791f6534dcfb4937dd5bdb69ae6f
(十六进制字符串表示,实际上为字节数组)3lZ5H2U03PtJN91b22mubw==
Content-MD5 Header: Content-MD5: 3lZ5H2U03PtJN91b22mubw==
StringToSign =HTTP-Verb + "\n" + //HTTP-Verb只支持POSTAccept + “\n” + //Accept为application/jsonContent-MD5 + "\n" + //第1步中计算出来的MD5值Content-Type + "\n" + //Content-Type值为application/jsonDate + "\n" + //Date值为GMT时间x-langboat-signature-method + "\n" + // 只支持 HMAC-SHA256x-langboat-signature-nonce + "\n";
StringToSign 示例:
POSTapplication/json3lZ5H2U03PtJN91b22mubw==application/jsonMon, 10 Oct 2022 07:11:08 GMTHMAC-SHA25642889
action=translateDoc&domain=general&sourceLanguage=zh&targetLanguage=en
stringToSign = headerStringToSign + queryToSign;
Signature = Base64(HMAC-SHA256( AccessSecret, UTF-8-Encoding-Of(stringToSign)))Authorization = AccessKey + ":" + Signature
Signature 示例:LaglFdItD0M/XD/hx0n0bglt0xTlKPuYhRWn4++dHfs=
Authorization 示例:
Authorization: 7Bo9ByyiTWRC1Y8KJJQ9cWtNpZLmrgyb:LaglFdItD0M/XD/hx0n0bglt0xTlKPuYhRWn4++dHfs=
QUERY参数 | 是否必须 | 描述 |
---|---|---|
action | 是 | 文本翻译固定参数:translateDoc |
domain | 是 | 领域代码, 例如通用 general , 支持的代码见 领域代码说明 |
sourceLanguage | 是 | 源语言代码,例如简体中文 zh ,支持的代码见 语种代码说明 |
targetLanguage | 是 | 目标语言代码,例如英语 en ,支持的代码见 语种代码说明 |
memoryID | 否 | 记忆库ID,指定您创建的记忆库ID,为空则不使用记忆库 |
BODY 参数 | 是否必须 | 描述 |
---|---|---|
fileContent | 是 | 待翻译文档内容的Base64编码,文档大小限制:5M |
filename | 是 | 文档标题 |
fileType | 是 | 文档类型,例如 docx |
成功的响应体是一个 JSON, 示例:
{"code": 0,"message": "success","data": { "docID": "448a2625-846a-4891-a48f-a43ed7117942" },"requestId": "402cd89f6e5fecc600c496af2ee63d4a"}
docID 为文档ID,后续用来下载文档
下面是可能的 HTTP 响应状态码、业务(错误)编码:
HTTP 状态码 | 业务编码 | 描述 |
---|---|---|
200 | 0 | 返回成功 |
400 | 10400 | 请求异常 |
401 | 10401 | 鉴权失败,核对 accessKey 和 accessSecret 是否正确 |
403 | 10403 | 权限不足,查看是否开通服务;或QPS,字符数,次数超过限制 |
422 | 10422 | 参数错误,核对请求参数 |
500 | 10500 | 服务异常 |
错误响应示例
{"code": 10422,"message": "参数错误,核对请求参数[ 不支持的domain : biology ]","requestId": "962132b206f8cedc77e41030b9aac2e6"}
# request:curl --request POST 'https://open.langboat-test.cc?action=translateDoc&domain=general&sourceLanguage=zh&memoryID=38&targetLanguage=en' \--header 'Content-MD5: cQvTn30Hx7G2S4E/SQgjBw==' \--header 'x-langboat-signature-method: HMAC-SHA256' \--header 'x-langboat-signature-nonce: 92508' \--header 'Authorization: tUGFmn3k3ty14o4d9iRBHq4INE******:f7s+9kLfIJWPB/uMSCySxDcjQLwdx7VJ92VqRsN/r+Y=' \--header 'Date: Wed, 30 Nov 2022 02:51:03 GMT' \--header 'Content-Type: application/json' \--data-raw '{"fileContent": "5L2g5aW977yM5LiW55WM","filename": "test.txt","fileType": "txt"}'# response:{"code":0,"message":"success","requestId":"402cd89f6e5fecc600c496af2ee63d4a","data":{"docID":"448a2625-846a-4891-a48f-a43ed7117942"}}
QUERY参数 | 是否必须 | 描述 |
---|---|---|
action | 是 | 文本翻译固定参数:translateDocDownload |
docID | 是 | 文档ID, 前述提交待翻译文档返回的 docID |
成功的响应体是一个 JSON, 示例:
{"code": 0,"message": "success","requestId": "f82e17bdfda8dc8f98e42fc20eae3867","data": {"domain": "general","sourceLanguage": "zh","targetLanguage": "en","filename": "test.txt","fileType": "txt","fileSize": 12,"fileMD5": "","fileContent": "5L2g5aW977yM5LiW55WM"}}
data 字段说明
字段名 | 类型 | 描述 |
---|---|---|
sourceLanguage | string | 原始语言 |
targetLanguage | string | 目标语言 |
filename | string | 文档标题 |
fileType | string | 文档类型, txt、docx |
fileSize | integer | 文档大小 |
fileMD5 | string | 文档MD5 |
fileContent | string | 翻译后文档的Base64编码 |
下面是可能的 HTTP 响应状态码、业务(错误)编码:
HTTP 状态码 | 业务编码 | 描述 |
---|---|---|
200 | 0 | 文档翻译完成 |
200 | 20001 | 文档未翻译完成 |
200 | 20002 | 文档翻译失败 |
400 | 10400 | 请求异常 |
401 | 10401 | 鉴权失败,核对 accessKey 和 accessSecret 是否正确 |
403 | 10403 | 权限不足,查看是否开通服务;或QPS,字符数,次数超过限制 |
422 | 10422 | 参数错误,核对请求参数 |
500 | 10500 | 服务异常 |
# request:curl --request GET 'https://open.langboat-test.cc?action=translateDocDownload&docID=448a2625-846a-4891-a48f-a43ed7117942' \--header 'Content-MD5: 1B2M2Y8AsgTpgAmY7PhCfg==' \--header 'x-langboat-signature-method: HMAC-SHA256' \--header 'x-langboat-signature-nonce: 92508' \--header 'Authorization: tUGFmn3k3ty14o4d9iRBHq4INE******:nhvdAy9711TuDHQ4kteFuarlscbtHHXclfb9LSW0Ao0=' \--header 'Date: Wed, 30 Nov 2022 02:58:57 GMT' \--header 'Content-Type: application/json'# response{"code": 0,"message": "success","requestId": "f82e17bdfda8dc8f98e42fc20eae3867","data": {"domain": "general","sourceLanguage": "zh","targetLanguage": "en","filename": "test.txt","fileType": "txt","fileSize": 12,"fileMD5": "","fileContent": "5L2g5aW977yM5LiW55WM"}}
api-example├── pom.xml├── src│ ├── main│ │ └── java│ │ └── com.langboat.saas.example│ │ ├── Examples.java│ │ └── LangboatOpenClient.java
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.langboat.saas.example</groupId><artifactId>api-example</artifactId><version>1.0.0</version><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><configuration><source>11</source><target>11</target></configuration></plugin></plugins></build><packaging>jar</packaging><name>api-example</name><properties><java.version>11</java.version><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.10</version></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.14.0</version></dependency></dependencies></project>
LangboatOpenClient.java
package com.langboat.saas.example;import cn.hutool.core.util.URLUtil;import com.fasterxml.jackson.databind.ObjectMapper;import javax.crypto.Mac;import javax.crypto.spec.SecretKeySpec;import java.io.*;import java.net.HttpURLConnection;import java.net.URL;import java.net.URLConnection;import java.security.MessageDigest;import java.text.SimpleDateFormat;import java.util.*;public class LangboatOpenClient {private final String accessKey;private final String accessSecret;private final String url;public LangboatOpenClient(String accessKey, String accessSecret) {this.accessKey = accessKey;this.accessSecret = accessSecret;this.url = "https://open.langboat.com";}public LangboatOpenClient(String accessKey, String accessSecret, String url) {this.accessKey = accessKey;this.accessSecret = accessSecret;this.url = url;}/** 计算 MD5 + Base64*/private String MD5Base64(String s) {if (s == null)return null;String encodeStr = "";byte[] utfBytes = s.getBytes();MessageDigest mdTemp;try {mdTemp = MessageDigest.getInstance("MD5");mdTemp.update(utfBytes);byte[] md5Bytes = mdTemp.digest();encodeStr = Base64.getEncoder().encodeToString(md5Bytes);} catch (Exception e) {throw new Error("Failed to generate MD5 : " + e.getMessage());}return encodeStr;}/** 计算 HMAC-SHA256 + Base64 编码*/private String HMACSha256Base64(String data, String key) {String result;try {SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), "HmacSHA256");Mac mac = Mac.getInstance("HmacSHA256");mac.init(signingKey);byte[] rawHmac = mac.doFinal(data.getBytes());result = Base64.getEncoder().encodeToString(rawHmac);} catch (Exception e) {throw new Error("Failed to generate HMAC : " + e.getMessage());}return result;}/** 获取时间*/private String toGMTString(Date date) {SimpleDateFormat df = new SimpleDateFormat("E, dd MMM yyyy HH:mm:ss z", Locale.CHINA);df.setTimeZone(new java.util.SimpleTimeZone(0, "GMT"));return df.format(date);}/*** 调用服务* @param queries: query 参数* @param data: request body 数据* @return*/public Object inference(Map<String, String> queries,Map<String, Object> data) {PrintWriter out = null;BufferedReader in = null;StringBuilder result = new StringBuilder();try {StringBuilder queriesStr = new StringBuilder();queries.forEach((k, v) -> queriesStr.append("&").append(k).append("=").append(URLUtil.encode(v)));queriesStr.setCharAt(0, '?');URL openUrl = new URL(this.url +queriesStr);String body = new ObjectMapper().writeValueAsString(data);String method = "POST";String accept = "application/json";String contentType = "application/json";String date = toGMTString(new Date());// 1.对body做MD5+BASE64加密String bodyMd5 = MD5Base64(body);String nonce = "" + (int) (Math.random() * 65535);String headerToSign = method + "\n" + accept + "\n" + bodyMd5 + "\n"+ contentType + "\n" + date + "\n"+ "HMAC-SHA256\n"+ nonce + "\n";// 2.计算 queryToSignList<String> queriesList = new ArrayList<>();queries.forEach((k, v) -> queriesList.add(k + "=" + v));Collections.sort(queriesList);String queryToSign = String.join("&", queriesList);// 3.计算 stringToSignString stringToSign = headerToSign + queryToSign;// 4.计算 HMAC-SHA256 + Base64String signature = HMACSha256Base64(stringToSign, this.accessSecret);// 5.得到 authorization header 值String authorization = this.accessKey + ":" + signature;URLConnection conn = openUrl.openConnection();conn.setRequestProperty("Accept", accept);conn.setRequestProperty("Content-Type", contentType);conn.setRequestProperty("Content-MD5", bodyMd5);conn.setRequestProperty("Date", date);conn.setRequestProperty("Authorization", authorization);conn.setRequestProperty("x-langboat-signature-nonce", nonce);conn.setRequestProperty("x-langboat-signature-method", "HMAC-SHA256");// POSTconn.setDoOutput(true);conn.setDoInput(true);out = new PrintWriter(conn.getOutputStream());// 发送请求参数out.print(body);// flush输出流的缓冲out.flush();// 定义BufferedReader输入流来读取URL的响应InputStream is;HttpURLConnection httpConn = (HttpURLConnection) conn;if (httpConn.getResponseCode() == 200) {is = httpConn.getInputStream();} else {is = httpConn.getErrorStream();}in = new BufferedReader(new InputStreamReader(is));String line;while ((line = in.readLine()) != null) {result.append(line);}} catch (IOException e) {e.printStackTrace();} finally {try {if (out != null) {out.close();}if (in != null) {in.close();}} catch (IOException ex) {ex.printStackTrace();}}return result.toString();}}
Examples.java
package com.langboat.saas.example;import cn.hutool.core.bean.BeanUtil;import cn.hutool.core.io.FileUtil;import cn.hutool.json.JSONUtil;import java.io.BufferedInputStream;import java.io.BufferedOutputStream;import java.io.IOException;import java.util.Base64;import java.util.Map;/*** API 调用示例*/public class Examples {private final LangboatOpenClient client = new LangboatOpenClient("yourAccessKey", "yourAccessSecret");/**** 提交待翻译文档* @param path 待翻译文档路径* @throws IOException 文件异常*/public Object submitFileToTranslate(String path) throws IOException {BufferedInputStream in = FileUtil.getInputStream(path);String fileContent = Base64.getEncoder().encodeToString(in.readAllBytes());Map<String, String> queries = Map.of("action", "translateDoc","domain", "general","sourceLanguage", "zh","targetLanguage", "en");Map<String, Object> body = Map.of("fileContent", fileContent,"filename", "test.txt","fileType", "txt");return this.client.inference(queries, body);}/**** 下载翻译后的文档, 解析base64编码的 fileContent 写入文件即可得到翻译后的文件* @param docID 提交待翻译文档后得到的文档ID* @param path 翻译后的文件保存的位置*/public void downloadTranslatedFile(String docID, String path) throws IOException {Map<String, String> queries = Map.of("action", "translateDocDownload","docID", docID);String o2 = (String) this.client.inference(queries, null);Map<String, Object> resp = JSONUtil.parseObj(o2);Map<String, Object> data = BeanUtil.beanToMap(resp.get("data"));String content = (String) data.get("fileContent");String filename = (String) data.get("filename");BufferedOutputStream out = FileUtil.getOutputStream(path + filename);out.write(Base64.getDecoder().decode(content));out.flush();}public static void main(String[] args) throws IOException {Examples examples = new Examples();Object res = examples.submitFileToTranslate("~/test.txt");System.out.println(res);examples.downloadTranslatedFile("628ad432-dac5-453c-92c6-31cd497852cb", "~/");}}
# -*- coding: utf-8 -*-import base64import datetimeimport hashlibimport hmacimport jsonimport randomimport requestsclass LangboatOpenClient:"""澜舟开放平台客户端"""def __init__(self,access_key: str,access_secret: str,url: str = "https://open.langboat.com"):self.access_key = access_keyself.access_secret = access_secretself.url = urldef _build_header(self, query: dict, data: dict) -> dict:accept = "application/json"# 1. body MD5 加密content_md5 = base64.b64encode(hashlib.md5(json.dumps(data).encode("utf-8")).digest()).decode()content_type = "application/json"gmt_format = '%a, %d %b %Y %H:%M:%S GMT'date = datetime.datetime.utcnow().strftime(gmt_format)signature_method = "HMAC-SHA256"signature_nonce = str(random.randint(0, 65535))header_string = f"POST\n{accept}\n{content_md5}\n{content_type}\n" \f"{date}\n{signature_method}\n{signature_nonce}\n"# 2. 计算 queryToSignqueries_str = []for k, v in sorted(query.items(), key=lambda item: item[0]):if isinstance(v, list):for i in v:queries_str.append(f"{k}={i}")else:queries_str.append(f"{k}={v}")queries_string = '&'.join(queries_str)# 3.计算 stringToSignsign_string = header_string + queries_string# 4.计算 HMAC-SHA256 + Base64secret_bytes = self.access_secret.encode("utf-8")# 5.计算签名signature = base64.b64encode(hmac.new(secret_bytes, sign_string.encode("utf-8"), hashlib.sha256).digest()).decode()res = {"Content-Type": content_type,"Content-MD5": content_md5,"Date": date,"Accept": accept,"X-Langboat-Signature-Method": signature_method,"X-Langboat-Signature-Nonce": signature_nonce,"Authorization": f"{self.access_key}:{signature}"}return resdef inference(self, queries: dict, data: dict) -> (int, dict):"""服务调用:param queries: query 参数:param data: request body 数据:return: response status, response body to json"""headers = self._build_header(queries, data)response = requests.post(url=self.url, headers=headers, params=queries, json=data)return response.status_code, response.json()if __name__ == '__main__':_access_key = '<Your access_key>'_access_secret = '<Your access_secret>'client = LangboatOpenClient(access_key=_access_key,access_secret=_access_secret)# 读取待翻译文档 text.txt(内容:"你好,世界")with open("test.txt", "rb") as f:content = base64.standard_b64encode(f.read())# 获取文件的 base64 编码字符串file_content = str(content, "utf-8")# 提交待翻译文档_queries = {"action": "translateDoc","domain": "general","sourceLanguage": "zh","targetLanguage": "en"}_data = {"fileContent": file_content,"filename": "test.txt","fileType": "txt"}status_code, result = client.inference(_queries, _data)print("response status:", status_code)print("response json:", json.dumps(result, ensure_ascii=False, indent=2))# 下载翻译完成的文档status_code, result = client.inference({"action": "translateDocDownload","docID": "54f60c11-6254-467a-8098-d806a1ad12ba", # 上面提交待翻译文档后返回的 docID}, {})print("response status:", status_code)print("response json:", json.dumps(result, ensure_ascii=False, indent=2))# 查看翻译后的文档内容print(base64.b64decode(result['data']['fileContent']))# 保存至文件 result.txtwith open("result.txt", 'wb') as f:f.write(base64.b64decode(result['data']['fileContent']))
package mainimport ("crypto/hmac""crypto/md5""crypto/sha256""encoding/base64""encoding/json""fmt""io/ioutil""log""math/rand""net/http""net/url""sort""strings""time")func main() {client := OpenClient{baseURL: "https://open.langboat.com",accessKey: "Your_Access_Key",accessSecret: "Your_Access_Secret",}// 提交待翻译文档queries := map[string]string{"action": "translateDoc","domain": "general","sourceLanguage": "zh","targetLanguage": "en",}// 读取文档获取 base64 编码字符串filename := "./test.docx"content, _ := ioutil.ReadFile(filename)encodedMessage := base64.StdEncoding.EncodeToString(content)data := map[string]interface{}{"fileContent": encodedMessage,"filename": "test.docx","fileType": "docx",}resp := client.Inference(queries, data)response, ok := resp.(*http.Response)if !ok {log.Fatal("fail to convert response")}body, err := ioutil.ReadAll(response.Body)if err != nil {log.Fatal("fail to read response body")}log.Println(string(body))// 下载翻译后的文档queries = map[string]string{"action": "translateDocDownload","docID": "54f60c11-6254-467a-8098-d806a1ad12ba",}resp = client.Inference(queries, nil)response, ok = resp.(*http.Response)if !ok {log.Fatal("fail to convert response")}body, err = ioutil.ReadAll(response.Body)if err != nil {log.Fatal("fail to read response body")}// 读取正文内容type RetData struct {SourceLanguage string `json:"source_language"` // 原始语言TargetLanguage string `json:"target_language"` // 目标语言Filename string `json:"file_name"` // 文档标题FileType string `json:"file_type"` // 文档类型FileSize int32 `json:"file_size"` // 文档大小FileMD5 string `json:"file_md5"` // 文档MD5FileContent string `json:"file_content"` // 文档的Base64编码}type DownloadResp struct {Code int32 `json:"code"`Message string `json:"message"`RequestID string `json:"requestId"`Data RetData `json:"data"`}var ret DownloadResp_ = json.Unmarshal(body, &ret)fileContent, _ := base64.StdEncoding.DecodeString(ret.Data.FileContent)// 翻译结果写入文件_ = ioutil.WriteFile("./"+ret.Data.Filename, fileContent, 0777)}type OpenClient struct {baseURL stringaccessKey stringaccessSecret string}// Inference 调用服务。queries: query 参数;data: request body 数据func (c *OpenClient) Inference(queries map[string]string, data map[string]interface{}) interface{} {var queriesStr = ""var first = truefor k, v := range queries {if first {queriesStr += "?" + k + "=" + url.QueryEscape(v)first = false} else {queriesStr += "&" + k + "=" + url.QueryEscape(v)}}dataJson, err := json.Marshal(data)if err != nil {log.Fatal(err.Error())}targetURL := c.baseURL + queriesStrclient := &http.Client{Timeout: 15 * time.Second,}// 构造headervar (payload = strings.NewReader(string(dataJson))date = time.Now().UTC().Format(http.TimeFormat)nonce = fmt.Sprint(10000 + rand.Intn(89999)))// 签名signParam := SignParam{Body: string(dataJson),Query: queriesStr[1:],DateGMT: date,Nonce: nonce,}contentMD5, signature := GenSignature(signParam, c.accessSecret)// 设置headerheaders := map[string]string{"Authorization": c.accessKey + ":" + signature,"Content-Type": "application/json","Accept": "application/json","Date": date,"Content-MD5": contentMD5,"x-langboat-signature-method": "HMAC-SHA256","x-langboat-signature-nonce": nonce,}req, _ := http.NewRequest("POST", targetURL, payload)for k, v := range headers {req.Header.Add(k, v)}resp, err := client.Do(req)if err != nil {log.Println(err.Error())}return resp}// SignParam 生成签名需要的参数type SignParam struct {Body string // body数据Query string // 原始queryDateGMT string // GTM时间Nonce string // 随机数}func getMD5(str string) []byte {h := md5.New()h.Write([]byte(str))return h.Sum(nil)}func hmacSha256(data string, secret string) []byte {h := hmac.New(sha256.New, []byte(secret))h.Write([]byte(data))return h.Sum(nil)}func resortQuery(src string) string {queries, _ := url.ParseQuery(src)keys := make([]string, 0)for k := range queries {keys = append(keys, k)}sort.Strings(keys)newQuery := url.Values{}for _, k := range keys {for _, value := range queries[k] {newQuery.Add(k, value)}}return newQuery.Encode()}// GenSignature 生成签名func GenSignature(src SignParam, apiSecret string) (string, string) {// 计算body的md5值md5str := getMD5(src.Body)// base64后得到contentMD5contentMD5 := base64.StdEncoding.EncodeToString(md5str)// query解析,并按照字典序重新排列query := resortQuery(src.Query)query, _ = url.QueryUnescape(query)// 需要做签名的字符串结构stringToSign := `POSTapplication/json%sapplication/json%sHMAC-SHA256%s%s`stringToSign = fmt.Sprintf(stringToSign, contentMD5, src.DateGMT, src.Nonce, query)hmac256 := hmacSha256(stringToSign, apiSecret)signature := base64.StdEncoding.EncodeToString(hmac256)return contentMD5, signature}
Products
Business Cooperation Email
Address
Floor 16, Fangzheng International Building, No. 52 Beisihuan West Road, Haidian District, Beijing, China.
© 2023, Langboat Co., Limited. All rights reserved.
Large Model Registration Code:Beijing-MengZiGPT-20231205
Business Cooperation:
bd@langboat.com
Address:
Floor 16, Fangzheng International Building, No. 52 Beisihuan West Road, Haidian District, Beijing, China.
Official Accounts:
© 2023, Langboat Co., Limited. All rights reserved.
Large Model Registration Code:Beijing-MengZiGPT-20231205