diff --git a/src/stores/player/slices/source.ts b/src/stores/player/slices/source.ts index 5fe7f395..dab67ce2 100644 --- a/src/stores/player/slices/source.ts +++ b/src/stores/player/slices/source.ts @@ -464,7 +464,7 @@ export const createSourceSlice: MakeSlice = (set, get) => ({ return; } - let cancelled = false; + const abortController = new AbortController(); set((s) => { s.caption.translateTask = { @@ -476,16 +476,16 @@ export const createSourceSlice: MakeSlice = (set, get) => ({ if (!this.done && !this.error) { console.log("Translation task was cancelled"); } - cancelled = true; + abortController.abort(); }, }; }); function handleError(err: any) { - console.error("Translation task ran into an error", err); - if (cancelled) { + if (abortController.signal.aborted) { return; } + console.error("Translation task ran into an error", err); set((s) => { if (!s.caption.translateTask) return; s.caption.translateTask.error = true; @@ -494,7 +494,7 @@ export const createSourceSlice: MakeSlice = (set, get) => ({ try { const srtData = await downloadCaption(targetCaption); - if (cancelled) { + if (abortController.signal.aborted) { return; } if (!srtData) { @@ -519,8 +519,9 @@ export const createSourceSlice: MakeSlice = (set, get) => ({ store.caption.translateTask!.fetchedTargetCaption!, targetLanguage, googletranslate, + abortController.signal, ); - if (cancelled) { + if (abortController.signal.aborted) { return; } if (!result) { diff --git a/src/utils/translation/googletranslate.ts b/src/utils/translation/googletranslate.ts index 47ceafc6..59e93149 100644 --- a/src/utils/translation/googletranslate.ts +++ b/src/utils/translation/googletranslate.ts @@ -12,23 +12,30 @@ export default { getConfig() { return { - singleBatchSize: 15, - multiBatchSize: 80, + single: { + batchSize: 250, + batchDelayMs: 1000, + }, + multi: { + batchSize: 80, + batchDelayMs: 200, + }, maxRetryCount: 3, - batchSleepMs: 200, }; }, - async translate(str, targetLang) { + async translate(str, targetLang, abortSignal) { if (!str) { return ""; } + str = str.replaceAll("\n", "
"); const response = await ( await fetch( `${SINGLE_API_URL}&tl=${targetLang}&q=${encodeURIComponent(str)}`, { method: "GET", + signal: abortSignal, headers: { Accept: "application/json", }, @@ -43,17 +50,20 @@ export default { return (response.sentences as any[]) .map((s: any) => s.trans as string) - .join(""); + .join("") + .replaceAll("
", "\n"); }, - async translateMulti(batch, targetLang) { + async translateMulti(batch, targetLang, abortSignal) { if (!batch || batch.length === 0) { return []; } + batch = batch.map((s) => s.replaceAll("\n", "
")); const response = await ( await fetch(BATCH_API_URL, { method: "POST", + signal: abortSignal, headers: { "Content-Type": "application/json+protobuf", "X-goog-api-key": BATCH_API_KEY, @@ -67,6 +77,8 @@ export default { throw new Error("Invalid response"); } - return response[0].map((s: any) => s as string); + return response[0].map((s: any) => + (s as string).replaceAll("
", "\n"), + ); }, } satisfies TranslateService; diff --git a/src/utils/translation/index.ts b/src/utils/translation/index.ts index 713f8c0b..249d9401 100644 --- a/src/utils/translation/index.ts +++ b/src/utils/translation/index.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import subsrt from "subsrt-ts"; import { Caption, ContentCaption } from "subsrt-ts/dist/types/handler"; @@ -7,16 +8,32 @@ import { compressStr, decompressStr, sleep } from "./utils"; const CAPTIONS_CACHE: Map = new Map(); +// single will not be used if multi-line is supported +export interface TranslateServiceConfig { + single: { + batchSize: number; + batchDelayMs: number; + }; + multi?: { + batchSize: number; + batchDelayMs: number; + }; + maxRetryCount: number; +} + export interface TranslateService { getName(): string; - getConfig(): { - singleBatchSize: number; - multiBatchSize: number; // -1 = unsupported - maxRetryCount: number; - batchSleepMs: number; - }; - translate(str: string, targetLang: string): Promise; - translateMulti(batch: string[], targetLang: string): Promise; + getConfig(): TranslateServiceConfig; + translate( + str: string, + targetLang: string, + abortSignal?: AbortSignal, + ): Promise; + translateMulti( + batch: string[], + targetLang: string, + abortSignal?: AbortSignal, + ): Promise; } class Translator { @@ -30,10 +47,21 @@ class Translator { private service: TranslateService; - constructor(srtData: string, targetLang: string, service: TranslateService) { + private serviceCfg: TranslateServiceConfig; + + private abortSignal?: AbortSignal; + + constructor( + srtData: string, + targetLang: string, + service: TranslateService, + abortSignal?: AbortSignal, + ) { this.captions = subsrt.parse(srtData); this.targetLang = targetLang; this.service = service; + this.serviceCfg = service.getConfig(); + this.abortSignal = abortSignal; for (const caption of this.captions) { if (caption.type !== "caption") { @@ -64,8 +92,15 @@ class Translator { while (!result && attempts < 3) { try { - result = await this.service.translate(content.text, this.targetLang); + result = await this.service.translate( + content.text, + this.targetLang, + this.abortSignal, + ); } catch (err) { + if (this.abortSignal?.aborted) { + break; + } console.warn("Translation attempt failed"); errors.push(err); await sleep(500); @@ -73,6 +108,10 @@ class Translator { } } + if (this.abortSignal?.aborted) { + return false; + } + if (!result) { console.warn("Translation failed", errors); return false; @@ -88,6 +127,7 @@ class Translator { const result = await this.service.translateMulti( batch.map((content) => content.text), this.targetLang, + this.abortSignal, ); if (result.length !== batch.length) { @@ -106,6 +146,9 @@ class Translator { return true; } catch (err) { + if (this.abortSignal?.aborted) { + return false; + } console.warn("Batch translation failed", err); return false; } @@ -113,10 +156,9 @@ class Translator { takeBatch(): ContentCaption[] { const batch: ContentCaption[] = []; - const batchSize = - this.service.getConfig().multiBatchSize === -1 - ? this.service.getConfig().singleBatchSize - : this.service.getConfig().multiBatchSize; + const batchSize = !this.serviceCfg.multi + ? this.serviceCfg.single.batchSize + : this.serviceCfg.multi!.batchSize; let count = 0; while (count < batchSize && this.contentCaptions.length > 0) { @@ -132,12 +174,24 @@ class Translator { } async translate(): Promise { + const batchDelay = !this.serviceCfg.multi + ? this.serviceCfg.single.batchDelayMs + : this.serviceCfg.multi!.batchDelayMs; + + console.info( + "Translating captions", + this.service.getName(), + this.contentCaptions.length, + batchDelay, + ); + console.time("translation"); + let batch: ContentCaption[] = this.takeBatch(); while (batch.length > 0) { let result: boolean; - console.info("Translating captions batch", batch.length, batch); + console.info("Translating batch", batch.length, batch); - if (this.service.getConfig().multiBatchSize === -1) { + if (!this.serviceCfg.multi) { result = ( await Promise.all( batch.map((content) => this.translateContent(content)), @@ -147,18 +201,24 @@ class Translator { result = await this.translateContentBatch(batch); } + if (this.abortSignal?.aborted) { + return undefined; + } + if (!result) { - console.error( - "Failed to translate captions batch", - batch.length, - batch, - ); + console.error("Failed to translate batch", batch.length, batch); return undefined; } batch = this.takeBatch(); - await sleep(this.service.getConfig().batchSleepMs); + await sleep(batchDelay); } + + if (this.abortSignal?.aborted) { + return undefined; + } + + console.timeEnd("translation"); return subsrt.build(this.captions, { format: "srt" }); } } @@ -167,6 +227,7 @@ export async function translate( caption: PlayerCaption, targetLang: string, service: TranslateService, + abortSignal?: AbortSignal, ): Promise { const cacheID = `${caption.id}_${targetLang}`; @@ -175,10 +236,15 @@ export async function translate( return decompressStr(cachedData); } - const translator = new Translator(caption.srtData, targetLang, service); + const translator = new Translator( + caption.srtData, + targetLang, + service, + abortSignal, + ); const result = await translator.translate(); - if (!result) { + if (!result || abortSignal?.aborted) { return undefined; }