基于划词翻译和 Azure OpenAI 实现 ChatGPT 在线翻译功能

作为技术人员平时经常会翻看一些技术文档,而对应技术社区来说很多文档都是英文的,那毫无疑问翻译工具是少不了的。了不起常用的一个翻译工具叫划词翻译,是一个 Chrome 插件,日常看英文文档的遇到不懂的单词或者句子直接选中,然后点击一下图标就可以实现自动翻译。

翻译源

这个插件的翻译源有很多,如下所示

其中的翻译源的使用方式不完全相同,有一些不用任何配置就可以使用,比如 DeepL 和必应翻译,安装好插件就可以直接使用;

有一些需要解决网络问题才能正常使用,比如谷歌翻译,因为谷歌翻译已经退出中国了;

还有一些需要去对应的官网申请秘钥进行配置过后才能正常使用,比如有道翻译和火山翻译等,如果上图所示。

配置 ChatGPT

通过翻译源我们可以看到其中也有 ChatGPT 的选项,ChatGPT 的配置相较于其他的翻译源我们除了要配置 API Key 之外,我们同时也需要解决网络问题。

但是这对于大部分来说,这两个问题都有一定的门槛,ChatGPT 对网络的要求稍微会高点,弄不好就会被封号。

所以我们这里通过两步来解决这个问题

  1. 申请微软的 Azure Open AI
  2. 通过 proxy 代理的形式将 ChatGPT 原生请求转换为 Azure Open AI 格式;

关于第一点前面的文章已经很详细的介绍了如果去申请,感兴趣的可以去看看,今天主要分享一下如何配置代理服务。

配置代理服务

在完成微软的 Azure Open AI 申请并成功部署模型过后,我们会获取到对应的 endpoint 以及秘钥信息。

然后我们注册一个 cloudflare 账号,然后在 Worker & Papes 下面创建一个应用,

重命名我们的服务,然后点击部署即可

然后点击 edit code,在编辑器中输入下放代码,其中的代码有三处需要修改

  1. resourceName:修改成申请的微软 Azure OpenAI 的时候配置的资源名称
  2. mapper:修改成 open ai 模型和 Azure OpenAI 的部署名称,前面是原生的模型名称,后面是 Azure 的部署名称;
  3. apiVersion:修改成在 Azure OpenAI 里面的一致即可;
代码语言:javascript
复制
// The name of your Azure OpenAI Resource.
const resourceName="resource name"
// The deployment name you chose when you deployed the model.
const mapper = {
  // model: deployName
    'gpt-35-turbo-16k': "gpt-35-turbo"
};
const apiVersion="2023-03-15-preview"

addEventListener("fetch", (event) => {
event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
if (request.method === 'OPTIONS') {
return handleOPTIONS(request)
}

const url = new URL(request.url);
if (url.pathname.startsWith("//")) {
url.pathname = url.pathname.replace('/',"")
}
if (url.pathname === '/v1/chat/completions') {
var path="chat/completions"
} else if (url.pathname === '/v1/completions') {
var path="completions"
} else if (url.pathname === '/v1/models') {
return handleModels(request)
} else {
return new Response('404 Not Found', { status: 404 })
}

let body;
if (request.method === 'POST') {
body = await request.json();
}

const modelName = body?.model;
const deployName = mapper[modelName] || ''

if (deployName === '') {
return new Response('Missing model mapper', {
status: 403
});
}
const fetchAPI = https://${resourceName}.openai.azure.com/openai/deployments/${deployName}/${path}?api-version=${apiVersion}

const authKey = request.headers.get('Authorization');
if (!authKey) {
return new Response("Not allowed", {
status: 403
});
}

const payload = {
method: request.method,
headers: {
"Content-Type": "application/json",
"api-key": authKey.replace('Bearer ', ''),
},
body: typeof body === 'object' ? JSON.stringify(body) : '{}',
};

let response = await fetch(fetchAPI, payload);
response = new Response(response.body, response);
response.headers.set("Access-Control-Allow-Origin", "*");

if (body?.stream != true){
return response
}

let { readable, writable } = new TransformStream()
stream(response.body, writable);
return new Response(readable, response);

}

function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

// support printer mode and add newline
async function stream(readable, writable) {
const reader = readable.getReader();
const writer = writable.getWriter();

// const decoder = new TextDecoder();
const encoder = new TextEncoder();
const decoder = new TextDecoder();
// let decodedValue = decoder.decode(value);
const newline = "\n";
const delimiter = "\n\n"
const encodedNewline = encoder.encode(newline);

let buffer = "";
while (true) {
let { value, done } = await reader.read();
if (done) {
break;
}
buffer += decoder.decode(value, { stream: true }); // stream: true is important here,fix the bug of incomplete line
let lines = buffer.split(delimiter);

// Loop through all but the last line, which may be incomplete.
for (let i = 0; i < lines.length - 1; i++) {
  await writer.write(encoder.encode(lines[i] + delimiter));
  await sleep(20);
}

buffer = lines[lines.length - 1];

}

if (buffer) {
await writer.write(encoder.encode(buffer));
}
await writer.write(encodedNewline)
await writer.close();
}

async function handleModels(request) {
const data = {
"object": "list",
"data": []
};

for (let key in mapper) {
data.data.push({
"id": key,
"object": "model",
"created": 1677610602,
"owned_by": "openai",
"permission": [{
"id": "modelperm-M56FXnG1AsIr3SXq8BYPvXJA",
"object": "model_permission",
"created": 1679602088,
"allow_create_engine": false,
"allow_sampling": true,
"allow_logprobs": true,
"allow_search_indices": false,
"allow_view": true,
"allow_fine_tuning": false,
"organization": "*",
"group": null,
"is_blocking": false
}],
"root": key,
"parent": null
});
}

const json = JSON.stringify(data, null, 2);
return new Response(json, {
headers: { 'Content-Type': 'application/json' },
});
}

async function handleOPTIONS(request) {
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': '',
'Access-Control-Allow-Methods': '
',
'Access-Control-Allow-Headers': '*'
}
})
}

代码修改完成过后,点击右上角的 save and deploy 按钮即可。此时 cloudflare 会帮我们生成一个 worker.dev 接口的地址,通过这个地址我们就可以直接访问了,不过更优雅的方式是我们配置一个自己的域名。

配置自定义域名

配置自定义域名要求我们首先有一个主域名,并且绑定到 cloudflare 里面,我们通过上方的 add site 添加一个自己的域名,

并且选择 free 方案

接下来我们再按照要求,到域名 dns 解析的地方去配置一下 dns 的解析,保存过后可以点击 Check nameservers,这个过程需要一段时间,上面说到差不多要 24 小时,我们可以晚点再回来看看。

直到我们看到对应的 site 下面显示 active 就说明可以了

接下来我们再到之前部分的 worker 下面去配置自定义域名

先配置一下 route,然后在配置一下自定义域名即可。

这里有点绕,不过按照流程配置是可以正常使用的。
配置 roure 的时候需要我们有可用是 site 站点;配置自定义域名的时候要求我们可用的 roure

配置划词翻译

当我们代理服务部署完成过后,再回来划词翻译这里,在插件的服务申请 => ChatGPT 管理秘钥这里,填入我们 Azure OpenAIkey;在第三方服务 => ChatGPT 这里填上我们上面自定义的域名和对应的模型名称,这个模型需要跟脚本 mapper 里面的 key 保持一致。

至此我们的 ChatGPT 的配置就完成了,简单总结一下:

因为我们国内无法直接使用 ChatGPT,我们这里通过申请微软的 Azure OpenAI 来替代原生 OpenAI,但是又因为划词翻译的 API 对接的是 OpenAI 的接口,所以我们需要一个中间代理服务将两者的协议进行转换。

如果划词翻译能直接兼容 Azure OpenAI 的话,其实我们就不用中间的代理了,可以直接配置。同理我们配置了代理服务过后,以后在其他 OpenAI 的客户端我们都可以直接使用了,一劳永逸。