Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ feat(gpt): 允许用户指定使用 GPT 翻译的时候是否开启 stream 模式 #947

Merged
merged 3 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions addon/locale/en-US/addon.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,5 @@ itemmenu-retranslateAbstract-label=Retranslate Abstract

field-titleTranslation=Title Translation
field-abstractTranslation=Abstract Translation

status-translating=Translating...
2 changes: 2 additions & 0 deletions addon/locale/it-IT/addon.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,5 @@ itemmenu-retranslateAbstract-label=Ritraduci Abstract

field-titleTranslation=Traduzione del titolo
field-abstractTranslation=Traduzione dell'Abstract

status-translating=Traduzione in corso...
2 changes: 2 additions & 0 deletions addon/locale/zh-CN/addon.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,5 @@ itemmenu-retranslateAbstract-label=重新翻译摘要

field-titleTranslation=标题翻译
field-abstractTranslation=摘要翻译

status-translating=正在翻译...
2 changes: 2 additions & 0 deletions addon/prefs.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ pref(
"__prefsPrefix__.chatGPT.prompt",
"As an academic expert with specialized knowledge in various fields, please provide a proficient and precise translation translation from ${langFrom} to ${langTo} of the academic text enclosed in 🔤. It is crucial to maintaining the original phrase or sentence and ensure accuracy while utilizing the appropriate language. The text is as follows: 🔤 ${sourceText} 🔤 Please provide the translated result without any additional explanation and remove 🔤.",
);
pref("__prefsPrefix__.chatGPT.stream", true);
pref("__prefsPrefix__.azureGPT.endPoint", "");
pref("__prefsPrefix__.azureGPT.model", "");
pref("__prefsPrefix__.azureGPT.apiVersion", "2023-05-15");
Expand All @@ -60,6 +61,7 @@ pref(
"__prefsPrefix__.azureGPT.prompt",
"As an academic expert with specialized knowledge in various fields, please provide a proficient and precise translation translation from ${langFrom} to ${langTo} of the academic text enclosed in 🔤. It is crucial to maintaining the original phrase or sentence and ensure accuracy while utilizing the appropriate language. The text is as follows: 🔤 ${sourceText} 🔤 Please provide the translated result without any additional explanation and remove 🔤.",
);
pref("__prefsPrefix__.azureGPT.stream", true);
pref(
"__prefsPrefix__.gemini.endPoint",
"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest",
Expand Down
123 changes: 86 additions & 37 deletions src/modules/services/gpt.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { TranslateTask, TranslateTaskProcessor } from "../../utils/task";
import { getPref } from "../../utils/prefs";
import { getString } from "../../utils/locale";

const gptTranslate = async function (
apiURL: string,
model: string,
temperature: number,
prefix: string,
data: Required<TranslateTask>,
stream?: boolean
) {
function transformContent(
langFrom: string,
Expand All @@ -19,6 +21,79 @@ const gptTranslate = async function (
.replaceAll("${sourceText}", sourceText);
}

const streamMode = stream ?? true;

//It takes some time to translate, so set the text to "Translating" before the request
if (streamMode === false) {
data.result = getString('status-translating');
addon.hooks.onReaderPopupRefresh();
addon.hooks.onReaderTabPanelRefresh();
}

/**
* The requestObserver callback, under streaming mode
* @param xmlhttp
*/
const streamCallback = (xmlhttp: XMLHttpRequest) => {
let preLength = 0;
let result = "";
xmlhttp.onprogress = (e: any) => {
// Only concatenate the new strings
const newResponse = e.target.response.slice(preLength);
const dataArray = newResponse.split("data: ");

for (const data of dataArray) {
try {
const obj = JSON.parse(data);
const choice = obj.choices[0];
if (choice.finish_reason) {
break;
}
result += choice.delta.content || "";
} catch {
continue;
}
}

// Clear timeouts caused by stream transfers
if (e.target.timeout) {
e.target.timeout = 0;
}

// Remove \n\n from the beginning of the data
data.result = result.replace(/^\n\n/, "");
preLength = e.target.response.length;

if (data.type === "text") {
addon.hooks.onReaderPopupRefresh();
addon.hooks.onReaderTabPanelRefresh();
}
};
}

/**
* The requestObserver callback, under non-streaming mode
* @param xmlhttp
*/
const nonStreamCallback = (xmlhttp: XMLHttpRequest) => {
// Non-streaming logic: Handle the complete response at once
xmlhttp.onload = () => {
// console.debug("GPT response received");
try {
const responseObj = JSON.parse(xmlhttp.responseText);
const resultContent = responseObj.choices[0].message.content;
data.result = resultContent.replace(/^\n\n/, "");
} catch (error) {
// throw `Failed to parse response: ${error}`;
return;
}

// Trigger UI updates after receiving the full response
addon.hooks.onReaderPopupRefresh();
addon.hooks.onReaderTabPanelRefresh();
};
}

const xhr = await Zotero.HTTP.request("POST", apiURL, {
headers: {
"Content-Type": "application/json",
Expand All @@ -34,44 +109,15 @@ const gptTranslate = async function (
},
],
temperature: temperature,
stream: true,
stream: streamMode,
}),
responseType: "text",
requestObserver: (xmlhttp: XMLHttpRequest) => {
let preLength = 0;
let result = "";
xmlhttp.onprogress = (e: any) => {
// Only concatenate the new strings
const newResponse = e.target.response.slice(preLength);
const dataArray = newResponse.split("data: ");

for (const data of dataArray) {
try {
const obj = JSON.parse(data);
const choice = obj.choices[0];
if (choice.finish_reason) {
break;
}
result += choice.delta.content || "";
} catch {
continue;
}
}

// Clear timeouts caused by stream transfers
if (e.target.timeout) {
e.target.timeout = 0;
}

// Remove \n\n from the beginning of the data
data.result = result.replace(/^\n\n/, "");
preLength = e.target.response.length;

if (data.type === "text") {
addon.hooks.onReaderPopupRefresh();
addon.hooks.onReaderTabPanelRefresh();
}
};
if (streamMode) {
streamCallback(xmlhttp);
} else {
nonStreamCallback(xmlhttp);
}
},
});
if (xhr?.status !== 200) {
Expand All @@ -84,18 +130,21 @@ export const chatGPT = <TranslateTaskProcessor>async function (data) {
const apiURL = getPref("chatGPT.endPoint") as string;
const model = getPref("chatGPT.model") as string;
const temperature = parseFloat(getPref("chatGPT.temperature") as string);
const stream = getPref("chatGPT.stream") as boolean;

return await gptTranslate(apiURL, model, temperature, "chatGPT", data);
return await gptTranslate(apiURL, model, temperature, "chatGPT", data, stream);
};

export const azureGPT = <TranslateTaskProcessor>async function (data) {
const endPoint = getPref("azureGPT.endPoint") as string;
const apiVersion = getPref("azureGPT.apiVersion");
const model = getPref("azureGPT.model") as string;
const temperature = parseFloat(getPref("azureGPT.temperature") as string);
const stream = getPref("azureGPT.stream") as boolean;

const apiURL = new URL(endPoint);
apiURL.pathname = `/openai/deployments/${model}/chat/completions`;
apiURL.search = `api-version=${apiVersion}`;

return await gptTranslate(apiURL.href, model, temperature, "azureGPT", data);
return await gptTranslate(apiURL.href, model, temperature, "azureGPT", data, stream);
};
21 changes: 21 additions & 0 deletions src/modules/settings/gpt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ async function gptStatusCallback(
temperature: parseFloat(getPref(`${prefix}.temperature`) as string),
prompt: getPref(`${prefix}.prompt`),
apiVersion: getPref("azureGPT.apiVersion"),
stream: getPref(`${prefix}.stream`),
};

dialog
Expand Down Expand Up @@ -133,6 +134,25 @@ async function gptStatusCallback(
"data-prop": "value",
},
},
{
tag: "label",
namespace: "html",
attributes: {
for: "stream",
},
properties: {
innerHTML: 'Stream',
},
},
{
tag: "input",
id: "stream",
attributes: {
type: "checkbox",
"data-bind": "stream",
"data-prop": "checked",
},
},
],
},
false,
Expand All @@ -156,6 +176,7 @@ async function gptStatusCallback(
setPref(`${prefix}.endPoint`, dialogData.endPoint);
setPref(`${prefix}.model`, dialogData.model);
setPref(`${prefix}.prompt`, dialogData.prompt);
setPref(`${prefix}.stream`, dialogData.stream);
setPref("azureGPT.apiVersion", dialogData.apiVersion);
}
break;
Expand Down