Spaces:
Sleeping
Sleeping
Upload 7 files
Browse files- LICENSE +21 -0
- README.md +91 -10
- scf_bootstrap +2 -0
- server.js +62 -0
- test.js +15 -0
- translate.js +97 -0
LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
MIT License
|
2 |
+
|
3 |
+
Copyright (c) 2023 LegendLeo
|
4 |
+
|
5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6 |
+
of this software and associated documentation files (the "Software"), to deal
|
7 |
+
in the Software without restriction, including without limitation the rights
|
8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9 |
+
copies of the Software, and to permit persons to whom the Software is
|
10 |
+
furnished to do so, subject to the following conditions:
|
11 |
+
|
12 |
+
The above copyright notice and this permission notice shall be included in all
|
13 |
+
copies or substantial portions of the Software.
|
14 |
+
|
15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21 |
+
SOFTWARE.
|
README.md
CHANGED
@@ -1,10 +1,91 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# DeepLX Serverless
|
2 |
+
|
3 |
+
DeepLX 免费翻译API**腾讯云函数部署版**,与[原项目DeepLX](https://github.com/OwO-Network/DeepLX)的区别在于**利用了云函数的请求IP不固定的特性,极大程度上避免了`429`请求太频繁报错**
|
4 |
+
|
5 |
+
感谢原项目[OwO-Network/DeepLX](https://github.com/OwO-Network/DeepLX)提供的灵感,这是本项目的坚实基础
|
6 |
+
|
7 |
+
## Usage | 用法
|
8 |
+
|
9 |
+
### Prerequisites | 你需要准备什么
|
10 |
+
|
11 |
+
- 一台电脑或平板
|
12 |
+
- 一个腾讯旗下的账号或者手机号
|
13 |
+
|
14 |
+
### Deploy | 部署
|
15 |
+
|
16 |
+
在 [https://cloud.tencent.com/](https://cloud.tencent.com/) 注册账号
|
17 |
+
|
18 |
+
进入云函数控制台:[https://console.cloud.tencent.com/scf/list](https://console.cloud.tencent.com/scf/list)
|
19 |
+
|
20 |
+
依次点击【新建】->【从头开始】,然后按照以下配置,**没写出来的就不用管,使用默认设置**
|
21 |
+
|
22 |
+
- 函数类型:Web函数
|
23 |
+
- 函数名称:deeplx(名字随便取)
|
24 |
+
- 地域:任意(国内也可直连)
|
25 |
+
- 运行环境:Nodejs 16.13(或者更高的版本)
|
26 |
+
- 高级配置:
|
27 |
+
- 内存:64M
|
28 |
+
- 执行超时时间:60 秒
|
29 |
+
- 请求多并发:5 并发(个人体验下来,2个都行)
|
30 |
+
- 日志配置 -> 日志投递:启用(可以选择不开,开的话一个月应该几分钱)
|
31 |
+
- 函数代码:本地上传zip包([点我下载 ZIP 包](https://github.com/LegendLeo/deeplx-serverless/releases/download/v1.0.0/dist.zip))
|
32 |
+
- 触发器配置(这里可能要创建一个新的触发器):
|
33 |
+
- 默认触发器
|
34 |
+
- 触发别名/版本:默认流量
|
35 |
+
- 请求方法:ANY
|
36 |
+
- 发布环境:发布
|
37 |
+
- 鉴权方法:免鉴权
|
38 |
+
|
39 |
+
此时已部署完成,可以点击“完成”按钮,进入【函数管理】,点击【函数代码】,往下拉,找到【访问路径】并复制后续使用
|
40 |
+
|
41 |
+
|
42 |
+
### How to use | 如何使用
|
43 |
+
|
44 |
+
建议搭配浏览器插件沉浸式翻译一同使用,使用的时候需要把访问路径里的 `/release` 部分替换为翻译路径`translate`
|
45 |
+
|
46 |
+
例如:`https://service-aaaaa.gz.apigw.tencentcs.com/release/` 改为:`https://service-aaaaa.gz.apigw.tencentcs.com/translate`
|
47 |
+
|
48 |
+
请求示例:
|
49 |
+
|
50 |
+
``` bash
|
51 |
+
curl --location 'https://service-aaaaa.gz.apigw.tencentcs.com/translate' \
|
52 |
+
--header 'Content-Type: application/json' \
|
53 |
+
--data '{
|
54 |
+
"text": "你好,世界",
|
55 |
+
"source_lang": "zh",
|
56 |
+
"target_lang": "en"
|
57 |
+
}'
|
58 |
+
```
|
59 |
+
|
60 |
+
响应示例:
|
61 |
+
|
62 |
+
``` json
|
63 |
+
{
|
64 |
+
"code": 200,
|
65 |
+
"message": "success",
|
66 |
+
"data": "Hello, world.",
|
67 |
+
"source_lang": "zh",
|
68 |
+
"target_lang": "en",
|
69 |
+
"alternatives": ["Hello, World.", "Hello, world!", "Hi, world."]
|
70 |
+
}
|
71 |
+
```
|
72 |
+
|
73 |
+
#### 沉浸式翻译设置
|
74 |
+
|
75 |
+
1. 在浏览器上安装最新的 [沉浸式翻译](https://github.com/immersive-translate/immersive-translate/releases)。
|
76 |
+
2. 点击左下角的 "开发者设置"。启用测试版实验功能。
|
77 |
+
3. 翻译服务选中 `DeepLX(beta)`
|
78 |
+
3. 设置 URL 为刚才获取的访问路径(需带translate)。
|
79 |
+
|
80 |
+

|
81 |
+
|
82 |
+
## 自托管
|
83 |
+
|
84 |
+
尽管本项目是专为 serverless 适配的方案,但是也能使用自己提供服务器进行部署
|
85 |
+
|
86 |
+
``` bash
|
87 |
+
git clone https://github.com/LegendLeo/deeplx-serverless
|
88 |
+
cd deeplx-serverless
|
89 |
+
npm install
|
90 |
+
npm run start
|
91 |
+
```
|
scf_bootstrap
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
#!/bin/sh
|
2 |
+
/var/lang/node16/bin/node server.js
|
server.js
ADDED
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const express = require('express');
|
2 |
+
const bodyParser = require('body-parser');
|
3 |
+
const { translate } = require('./translate');
|
4 |
+
const cors = require('cors');
|
5 |
+
const fs = require('fs');
|
6 |
+
const app = express();
|
7 |
+
const PORT = 7860;
|
8 |
+
|
9 |
+
app.use(cors({ origin: '*' }));
|
10 |
+
app.use(bodyParser.json());
|
11 |
+
|
12 |
+
let delay = 0;
|
13 |
+
const delayIncrement = 100;
|
14 |
+
const maxDelay = 5000;
|
15 |
+
|
16 |
+
if (fs.existsSync('delay.txt')) {
|
17 |
+
delay = parseInt(fs.readFileSync('delay.txt', 'utf8'));
|
18 |
+
}
|
19 |
+
|
20 |
+
console.log('File system loaded')
|
21 |
+
|
22 |
+
app.post('/translate', async (req, res) => {
|
23 |
+
|
24 |
+
console.log('Translate start')
|
25 |
+
|
26 |
+
const { text, source_lang, target_lang } = req.body;
|
27 |
+
|
28 |
+
try {
|
29 |
+
await new Promise(resolve => setTimeout(resolve, delay));
|
30 |
+
|
31 |
+
const result = await translate(text, source_lang, target_lang);
|
32 |
+
const responseData = {
|
33 |
+
alternatives: result.alternatives,
|
34 |
+
code: 200,
|
35 |
+
data: result.text,
|
36 |
+
id: Math.floor(Math.random() * 10000000000),
|
37 |
+
method: 'Free',
|
38 |
+
source_lang,
|
39 |
+
target_lang,
|
40 |
+
};
|
41 |
+
res.json(responseData);
|
42 |
+
} catch (error) {
|
43 |
+
if (error.response && error.response.status === 429) {
|
44 |
+
|
45 |
+
delay += delayIncrement;
|
46 |
+
if (delay > maxDelay) {
|
47 |
+
delay = maxDelay;
|
48 |
+
}
|
49 |
+
fs.writeFileSync('delay.txt', delay.toString());
|
50 |
+
console.log(`429 에러 발생. 딜레이 증가: ${delay}ms`);
|
51 |
+
}
|
52 |
+
res.status(500).json({ error: 'Translation failed' });
|
53 |
+
}
|
54 |
+
});
|
55 |
+
|
56 |
+
app.get('/', (req, res) => {
|
57 |
+
res.send('서버가 구동되었습니다.');
|
58 |
+
});
|
59 |
+
|
60 |
+
app.listen(PORT, () => {
|
61 |
+
console.log(`Server is running on http://localhost:${PORT}`);
|
62 |
+
});
|
test.js
ADDED
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const translate = require('./translate');
|
2 |
+
|
3 |
+
;(async () => {
|
4 |
+
// Example Call
|
5 |
+
console.log(await translate('明天你好', 'ZH', 'EN', true, true));
|
6 |
+
console.log(
|
7 |
+
await translate(
|
8 |
+
'Generate a cryptographically strong random string',
|
9 |
+
'EN',
|
10 |
+
'ZH',
|
11 |
+
true,
|
12 |
+
true
|
13 |
+
)
|
14 |
+
);
|
15 |
+
})()
|
translate.js
ADDED
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const axios = require('axios').default;
|
2 |
+
const { random } = require('lodash');
|
3 |
+
|
4 |
+
const DEEPL_BASE_URL = 'https://www2.deepl.com/jsonrpc';
|
5 |
+
const headers = {
|
6 |
+
'Content-Type': 'application/json',
|
7 |
+
Accept: '*/*',
|
8 |
+
'x-app-os-name': 'iOS',
|
9 |
+
'x-app-os-version': '16.3.0',
|
10 |
+
'Accept-Language': 'en-US,en;q=0.9',
|
11 |
+
'Accept-Encoding': 'gzip, deflate, br',
|
12 |
+
'x-app-device': 'iPhone13,2',
|
13 |
+
'User-Agent': 'DeepL-iOS/2.9.1 iOS 16.3.0 (iPhone13,2)',
|
14 |
+
'x-app-build': '510265',
|
15 |
+
'x-app-version': '2.9.1',
|
16 |
+
Connection: 'keep-alive',
|
17 |
+
};
|
18 |
+
|
19 |
+
function getICount(translateText) {
|
20 |
+
return (translateText || '').split('i').length - 1;
|
21 |
+
}
|
22 |
+
|
23 |
+
function getRandomNumber() {
|
24 |
+
return random(8300000, 8399998) * 1000;
|
25 |
+
}
|
26 |
+
|
27 |
+
function getTimestamp(iCount) {
|
28 |
+
const ts = Date.now();
|
29 |
+
if (iCount === 0) {
|
30 |
+
return ts;
|
31 |
+
}
|
32 |
+
iCount++;
|
33 |
+
return ts - (ts % iCount) + iCount;
|
34 |
+
}
|
35 |
+
|
36 |
+
async function translate(
|
37 |
+
text,
|
38 |
+
sourceLang = 'AUTO',
|
39 |
+
targetLang = 'KO',
|
40 |
+
numberAlternative = 0,
|
41 |
+
printResult = false,
|
42 |
+
) {
|
43 |
+
const iCount = getICount(text);
|
44 |
+
const id = getRandomNumber();
|
45 |
+
|
46 |
+
numberAlternative = Math.max(Math.min(3, numberAlternative), 0);
|
47 |
+
|
48 |
+
const postData = {
|
49 |
+
jsonrpc: '2.0',
|
50 |
+
method: 'LMT_handle_texts',
|
51 |
+
id: id,
|
52 |
+
params: {
|
53 |
+
texts: [{ text: text, requestAlternatives: numberAlternative }],
|
54 |
+
splitting: 'newlines',
|
55 |
+
lang: {
|
56 |
+
source_lang_user_selected: sourceLang.toUpperCase(),
|
57 |
+
target_lang: targetLang.toUpperCase(),
|
58 |
+
},
|
59 |
+
timestamp: getTimestamp(iCount),
|
60 |
+
},
|
61 |
+
};
|
62 |
+
|
63 |
+
let postDataStr = JSON.stringify(postData);
|
64 |
+
|
65 |
+
if ((id + 5) % 29 === 0 || (id + 3) % 13 === 0) {
|
66 |
+
postDataStr = postDataStr.replace('"method":"', '"method" : "');
|
67 |
+
} else {
|
68 |
+
postDataStr = postDataStr.replace('"method":"', '"method": "');
|
69 |
+
}
|
70 |
+
|
71 |
+
try {
|
72 |
+
const response = await axios.post(DEEPL_BASE_URL, postDataStr, {
|
73 |
+
headers: headers,
|
74 |
+
});
|
75 |
+
|
76 |
+
if (response.status === 429) {
|
77 |
+
throw new Error(
|
78 |
+
`Too many requests, your IP has been blocked by DeepL temporarily, please don't request it frequently in a short time.`
|
79 |
+
);
|
80 |
+
}
|
81 |
+
|
82 |
+
if (response.status !== 200) {
|
83 |
+
console.error('Error', response.status);
|
84 |
+
return;
|
85 |
+
}
|
86 |
+
|
87 |
+
const result = response.data.result.texts[0]
|
88 |
+
if (printResult) {
|
89 |
+
console.log(result);
|
90 |
+
}
|
91 |
+
return result;
|
92 |
+
} catch (err) {
|
93 |
+
console.error(err.message);
|
94 |
+
}
|
95 |
+
}
|
96 |
+
|
97 |
+
exports.translate = translate;
|