From 2a69fb7f01174282db774751534942a14c0b36af Mon Sep 17 00:00:00 2001 From: Chen Yu Date: Sun, 27 Aug 2023 19:13:07 +0800 Subject: [PATCH] OpenAI npm v4 (#7) * OpenAI lib v4 Migration * 1.1.0 --- package-lock.json | 268 ++++++++++++++---- package.json | 15 +- src/axios.mjs | 3 - src/moderator.mjs | 6 +- src/openai.mjs | 62 ++-- src/subtitle.mjs | 2 +- src/translator.mjs | 88 ++++-- test/data/test_ja_small.srt.out_Chinese.srt | 2 +- .../test_ja_small.srt.progress_Chinese.csv | 2 +- 9 files changed, 309 insertions(+), 139 deletions(-) delete mode 100644 src/axios.mjs diff --git a/package-lock.json b/package-lock.json index 70e76ff..611f78f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,21 +1,21 @@ { "name": "chatgpt-subtitle-translator", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "chatgpt-subtitle-translator", - "version": "1.0.0", + "version": "1.1.0", "license": "MIT", "dependencies": { - "@types/node": "^18.14.6", - "commander": "^10.0.0", - "dotenv": "^16.0.3", + "@types/node": "^20.5.6", + "commander": "^11.0.0", + "dotenv": "^16.3.1", "gpt-3-encoder": "^1.1.4", - "openai": "^3.2.1", + "openai": "^4.3.0", "readline": "^1.3.0", - "srt-parser-2": "^1.2.2" + "srt-parser-2": "^1.2.3" }, "bin": { "chatgpt-subtitle-translator": "cli/translator.mjs" @@ -25,21 +25,57 @@ } }, "node_modules/@types/node": { - "version": "18.15.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.9.tgz", - "integrity": "sha512-dUxhiNzBLr6IqlZXz6e/rN2YQXlFgOei/Dxy+e3cyXTJ4txSUbGT2/fmnD6zd/75jDMeW5bDee+YXxlFKHoV0A==" + "version": "20.5.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.6.tgz", + "integrity": "sha512-Gi5wRGPbbyOTX+4Y2iULQ27oUPrefaB0PxGQJnfyWN3kvEDGM3mIB5M/gQLmitZf7A9FmLeaqxD3L1CXpm3VKQ==" + }, + "node_modules/@types/node-fetch": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.4.tgz", + "integrity": "sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==", + "dependencies": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, - "node_modules/axios": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", - "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", - "dependencies": { - "follow-redirects": "^1.14.8" + "node_modules/base-64": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", + "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" + }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "engines": { + "node": "*" } }, "node_modules/combined-stream": { @@ -54,11 +90,19 @@ } }, "node_modules/commander": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.0.tgz", - "integrity": "sha512-zS5PnTI22FIRM6ylNW8G4Ap0IEOyk62fhLSD0+uHRT9McRCLGpkVNvao4bjimpK/GShynyQkFFxHhwMcETmduA==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz", + "integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==", + "engines": { + "node": ">=16" + } + }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", "engines": { - "node": ">=14" + "node": "*" } }, "node_modules/delayed-stream": { @@ -69,37 +113,38 @@ "node": ">=0.4.0" } }, + "node_modules/digest-fetch": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/digest-fetch/-/digest-fetch-1.3.0.tgz", + "integrity": "sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==", + "dependencies": { + "base-64": "^0.1.0", + "md5": "^2.3.0" + } + }, "node_modules/dotenv": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", - "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" } }, - "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } + "node": ">=6" } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -109,11 +154,51 @@ "node": ">= 6" } }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, "node_modules/gpt-3-encoder": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/gpt-3-encoder/-/gpt-3-encoder-1.1.4.tgz", "integrity": "sha512-fSQRePV+HUAhCn7+7HL7lNIXNm6eaFWFbNLOOGtmSJ0qJycyQvj60OvRlH7mee8xAMjBDNRdMXlMwjAbMTDjkg==" }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -133,30 +218,113 @@ "node": ">= 0.6" } }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/openai": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/openai/-/openai-3.2.1.tgz", - "integrity": "sha512-762C9BNlJPbjjlWZi4WYK9iM2tAVAv0uUp1UmI34vb0CN5T2mjB/qM6RYBmNKMh/dN9fC+bxqPwWJZUTWW052A==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.3.0.tgz", + "integrity": "sha512-ObF5jxvZoQbCNAI6FiiNkzFDinBRbu4KPm73/PKCy9UvjI24kPpnN9kK56rtPDVYxfk78C0A2SK0bJAE633BuQ==", "dependencies": { - "axios": "^0.26.0", - "form-data": "^4.0.0" + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "digest-fetch": "^1.3.0", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" } }, + "node_modules/openai/node_modules/@types/node": { + "version": "18.17.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.11.tgz", + "integrity": "sha512-r3hjHPBu+3LzbGBa8DHnr/KAeTEEOrahkcL+cZc4MaBMTM+mk8LtXR+zw+nqfjuDZZzYTYgTcpHuP+BEQk069g==" + }, "node_modules/readline": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/readline/-/readline-1.3.0.tgz", "integrity": "sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg==" }, "node_modules/srt-parser-2": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/srt-parser-2/-/srt-parser-2-1.2.2.tgz", - "integrity": "sha512-SlWIhy9mQGMY8BHv1MkeQbCx6gHUNG0NecZro4QIDxzIpBw3Q0kFjA4EWyE9I+H7VrmdPYBHlvtrKzMLFTKP0g==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/srt-parser-2/-/srt-parser-2-1.2.3.tgz", + "integrity": "sha512-dANP1AyJTI503H0/kXwRza+7QxDB3BqeFvEKTF4MI9lQcBe8JbRUQTKVIGzGABJCwBovEYavZ2Qsdm/s8XKz8A==", "bin": { "srt-parser-2": "bin/index.js" }, "engines": { "node": "*" } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } } } } diff --git a/package.json b/package.json index cfa4d7a..d2c2e91 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chatgpt-subtitle-translator", - "version": "1.0.0", + "version": "1.1.0", "description": "Translation tool based on ChatGPT API", "main": "src/translator.mjs", "type": "module", @@ -10,19 +10,18 @@ "author": "Cerlancism", "license": "MIT", "dependencies": { - "@types/node": "^18.14.6", - "commander": "^10.0.0", - "dotenv": "^16.0.3", + "@types/node": "^20.5.6", + "commander": "^11.0.0", + "dotenv": "^16.3.1", "gpt-3-encoder": "^1.1.4", - "openai": "^3.2.1", + "openai": "^4.3.0", "readline": "^1.3.0", - "srt-parser-2": "^1.2.2" + "srt-parser-2": "^1.2.3" }, "engines": { "node": ">=16.13.0" }, "bin": { - "chatgpt-subtitle-translator": "./cli/translator.mjs", - "subtitle-util": "./cli/subtitle.mjs" + "chatgpt-subtitle-translator": "./cli/translator.mjs" } } diff --git a/src/axios.mjs b/src/axios.mjs deleted file mode 100644 index f5efb27..0000000 --- a/src/axios.mjs +++ /dev/null @@ -1,3 +0,0 @@ -import axios from "axios"; - -export const axiosStatic = axios.default diff --git a/src/moderator.mjs b/src/moderator.mjs index ec35c40..1dfac3d 100644 --- a/src/moderator.mjs +++ b/src/moderator.mjs @@ -18,8 +18,8 @@ export async function checkModeration(input) return await openaiRetryWrapper(async () => { await cooler.cool() - const moderation = await openai.createModeration({ input }) - const moderationData = moderation.data.results[0] + const moderation = await openai.moderations.create({ input }) + const moderationData = moderation.results[0] if (moderationData.flagged) { @@ -33,7 +33,7 @@ export async function checkModeration(input) } /** - * @param {import("openai").CreateModerationResponseResultsInner} moderatorOutput + * @param {import("openai").OpenAI.Moderation} moderatorOutput */ export function getModeratorResults(moderatorOutput) { diff --git a/src/openai.mjs b/src/openai.mjs index e2e1a56..3ec2837 100644 --- a/src/openai.mjs +++ b/src/openai.mjs @@ -3,8 +3,7 @@ import * as dotenv from 'dotenv' dotenv.config() -import { axiosStatic } from './axios.mjs'; -import { Configuration, OpenAIApi } from "openai"; +import { OpenAI } from "openai"; import { CooldownContext } from './cooldown.mjs'; import { retryWrapper, sleep } from './helpers.mjs'; import gp3Encoder from "gpt-3-encoder"; @@ -21,12 +20,11 @@ export const PrmoptTokenCostPer1k = { // 'gpt-4-32k': 0.12 // } -const configuration = new Configuration({ +export const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY, + maxRetries: 3 }); -export const openai = new OpenAIApi(configuration); - export const coolerAPI = new CooldownContext(Number(process.env.OPENAI_API_RPM ?? 60), 60000, "ChatGPTAPI") export const coolerModerator = new CooldownContext(Number(process.env.OPENAI_API_RPM ?? process.env.OPENAI_API_MODERATOR_RPM ?? 60), 60000, "OpenAIModerator") @@ -55,11 +53,11 @@ export async function openaiRetryWrapper(func, maxRetries, description) { const error = retryContext.error let delay = 1000 * retryContext.currentTry * retryContext.currentTry - if (axiosStatic.isAxiosError(error)) + if (error instanceof OpenAI.APIError) { - console.error(`[Error_${description}]`, new Date(), "Status", error.response?.status, error.name, error.message, error.response?.data?.error) + console.error(`[Error_${description}]`, new Date(), "Status", error.status, error.name, error.message, error.error) - if (error.response?.status === 429 || (error.response?.status >= 500 && error.response?.status <= 599)) + if (error.status === 429 || (error.status >= 500 && error.status <= 599)) { delay = delay * retryContext.currentTry } @@ -87,53 +85,33 @@ export async function openaiRetryWrapper(func, maxRetries, description) } /** - * @param {import("axios").AxiosResponse} response - * @return {Promise>} + * @param {import("openai/streaming").Stream} response + * @returns {Promise} */ export async function completeChatStream(response, onData = (d) => { }, onEnd = () => { }) { let output = '' - return await new Promise((resolve, reject) => + return await new Promise(async (resolve, reject) => { - response.data.on("data", (/** @type {Buffer} */ data) => + try { - const lines = data.toString().split('\n').filter(line => line.trim() !== ''); - for (const line of lines) + for await (const part of response) { - const message = line.replace(/^data: /, ''); - if (message === '[DONE]') - { - response.data = { - choices: [ - { message: { role: "assistant", content: output } } - ] - } - onEnd() - resolve(response) - return; // Stream finished - } - try + const text = part.choices[0].delta.content + if (text) { - const parsed = JSON.parse(message); - const text = parsed.choices[0]?.delta?.content ?? "" output += text - if (text) - { - onData(text) - } - - } catch (error) - { - const chatStreamError = new ChatStreamSyntaxError(`Could not JSON parse stream message: ${error.message} \npayload:\n${message}\n`, error) - reject(chatStreamError) + onData(text) } } - }) + onEnd() + resolve(output) - response.data.on("error", (e) => + } catch (error) { - reject(e) - }) + const chatStreamError = new ChatStreamSyntaxError(`Could not JSON parse stream message: ${error.message}`, error) + reject(chatStreamError) + } }) } diff --git a/src/subtitle.mjs b/src/subtitle.mjs index ed961e0..e942127 100644 --- a/src/subtitle.mjs +++ b/src/subtitle.mjs @@ -1,7 +1,7 @@ //@ts-check import srtParser2 from "srt-parser-2" -export const parser = new srtParser2.default(); +export const parser = new srtParser2(); /** * @param {string} text diff --git a/src/translator.mjs b/src/translator.mjs index f950014..b275ce6 100644 --- a/src/translator.mjs +++ b/src/translator.mjs @@ -7,9 +7,9 @@ import { roundWithPrecision, sleep } from './helpers.mjs'; /** * @type {TranslatorOptions} * @typedef TranslatorOptions - * @property {Pick, "messages" | "model"> & Omit} createChatCompletionRequest + * @property {Pick, "messages" | "model"> & Omit} createChatCompletionRequest * Options to ChatGPT besides the messages, it is recommended to set `temperature: 0` for a (almost) deterministic translation - * @property {import('openai').ChatCompletionRequestMessage[]} initialPrompts + * @property {import('openai').OpenAI.Chat.CreateChatCompletionRequestMessage[]} initialPrompts * Initiation prompt messages before the translation request messages * @property {boolean} useModerator `true` \ * Verify with the free OpenAI Moderation tool prior to submitting the prompt to ChatGPT model @@ -55,6 +55,7 @@ export class Translator this.openaiClient = openai this.systemInstruction = `Translate ${this.language.from ? this.language.from + " " : ""}to ${this.language.to}` + /** @type {import('openai').OpenAI.Chat.CreateChatCompletionRequestMessage[]} */ this.promptContext = [] this.cooler = coolerAPI @@ -74,12 +75,13 @@ export class Translator /** * @param {string} text + * @returns {Promise} */ async translatePrompt(text) { - /** @type {import('openai').ChatCompletionRequestMessage} */ + /** @type {import('openai').OpenAI.Chat.CreateChatCompletionRequestMessage} */ const userMessage = { role: "user", content: `${text}` } - /** @type {import('openai').ChatCompletionRequestMessage[]} */ + /** @type {import('openai').OpenAI.Chat.CreateChatCompletionRequestMessage[]} */ const systemMessage = this.systemInstruction ? [{ role: "system", content: `${this.systemInstruction}` }] : [] const messages = [...systemMessage, ...this.options.initialPrompts, ...this.promptContext, userMessage] @@ -88,18 +90,30 @@ export class Translator { await this.cooler.cool() startTime = Date.now() - const promptResponse = await openai.createChatCompletion({ - messages, - ...this.options.createChatCompletionRequest - }, this.options.createChatCompletionRequest.stream ? { responseType: "stream" } : undefined) if (!this.options.createChatCompletionRequest.stream) { + const promptResponse = await openai.chat.completions.create({ + messages, + ...this.options.createChatCompletionRequest, + stream: false, + }) endTime = Date.now() - return promptResponse + const output = new TranslationOutput( + promptResponse.choices[0].message.content, + promptResponse.usage?.prompt_tokens, + promptResponse.usage?.completion_tokens, + promptResponse.usage?.total_tokens + ) + return output } else { + const promptResponse = await openai.chat.completions.create({ + messages, + ...this.options.createChatCompletionRequest, + stream: true + }) let writeQueue = '' const streamOutput = await completeChatStream(promptResponse, /** @param {string} data */(data) => { @@ -125,13 +139,17 @@ export class Translator process.stdout.write("\n") }) const prompt_tokens = numTokensFromMessages(messages) - const completion_tokens = numTokensFromMessages([{ content: streamOutput.data.choices[0].message.content }]) - streamOutput.data.usage = { prompt_tokens, completion_tokens, total_tokens: prompt_tokens + completion_tokens } - return streamOutput + const completion_tokens = numTokensFromMessages([{ content: streamOutput }]) + const output = new TranslationOutput( + streamOutput, + prompt_tokens, + completion_tokens + ) + return output } }, 3, "TranslationPrompt") - this.tokensUsed += getTokens(response) + this.tokensUsed += response.totalTokens this.tokensProcessTimeMs += (endTime - startTime) return response } @@ -160,7 +178,7 @@ export class Translator } this.buildContext() const output = await this.translatePrompt(input) - const text = getPromptContent(output) + const text = output.content const writeOut = text.split("\n").join(" ") yield* this.yieldOutput([batch[x]], [writeOut]) } @@ -198,12 +216,12 @@ export class Translator } this.buildContext() const output = await this.translatePrompt(input) - const text = getPromptContent(output) + const text = output.content let outputs = text.split("\n").filter(x => x.length > 0) if (this.options.lineMatching && batch.length !== outputs.length) { - this.tokensWasted += getTokens(output) + this.tokensWasted += output.totalTokens console.error(`[Translator]`, "Lines count mismatch", batch.length, outputs.length) console.error(`[Translator]`, "batch", batch) console.error(`[Translator]`, "transformed", outputs) @@ -346,7 +364,7 @@ export class Translator return text } - this.promptContext = /** @type {import('openai').ChatCompletionRequestMessage[]}*/([ + this.promptContext = /** @type {import('openai').OpenAI.Chat.ChatCompletionMessage[]}*/([ { role: "user", content: sliced.map((x, i) => checkFlaggedMapper(x.source, i)).join("\n\n") }, { role: "assistant", content: sliced.map((x, i) => checkFlaggedMapper(x.transform, i)).join("\n\n") } ]) @@ -369,18 +387,28 @@ export class Translator } } -/** - * @param {import("axios").AxiosResponse} response - */ -function getTokens(response) +export class TranslationOutput { - return response.data.usage?.total_tokens ?? 0 -} + /** + * @param {string} content + * @param {number} promptTokens + * @param {number} completionTokens + * @param {number} [totalTokens] + */ + constructor(content, promptTokens, completionTokens, totalTokens) + { + this.content = content + this.promptTokens = promptTokens ?? 0 + this.completionTokens = completionTokens ?? 0 + this._totalTokens = totalTokens + } -/** - * @param {import("axios").AxiosResponse} openaiRes - */ -function getPromptContent(openaiRes) -{ - return openaiRes.data.choices[0].message?.content ?? "" -} + get totalTokens() + { + if (!this._totalTokens) + { + this._totalTokens = this.promptTokens + this.completionTokens + } + return this._totalTokens + } +} \ No newline at end of file diff --git a/test/data/test_ja_small.srt.out_Chinese.srt b/test/data/test_ja_small.srt.out_Chinese.srt index 4ad3b7a..ae77625 100644 --- a/test/data/test_ja_small.srt.out_Chinese.srt +++ b/test/data/test_ja_small.srt.out_Chinese.srt @@ -12,7 +12,7 @@ 4 00:00:08,000 --> 00:00:12,000 -今天天气很好。 +今天天气很好呢。 5 00:00:12,000 --> 00:00:16,000 diff --git a/test/data/test_ja_small.srt.progress_Chinese.csv b/test/data/test_ja_small.srt.progress_Chinese.csv index d177c49..d84a4ca 100644 --- a/test/data/test_ja_small.srt.progress_Chinese.csv +++ b/test/data/test_ja_small.srt.progress_Chinese.csv @@ -1,5 +1,5 @@ 1, "早上好。" 2, "您好吗?" 3, "是的,我很好。" -4, "今天天气很好。" +4, "今天天气很好呢。" 5, "是的,天气非常好。"