Skip to content

Commit dadee30

Browse files
feat(subtitles): batch pre-translation with silent background processing (#838)
* feat: init youtube subtitle * feat: youtube subtitles * feat: add youtube subtitles * feat: youtube subtitles * revert: revert use draggable * feat: improve interactive of youtube subtitles * chore: add changeset * feat: add config migrate example * fix: migrate example error * fix: improve interaction * fix: fix ai errors * fix: import error * chore: remove youtubei * fix: improve interaction * refactor: refactor subtitles.content * feat: add youtube dont walk element * fix: interceptor mismatch * fix: remove --disable-blink-features=AutomationControlled * fix: remove useless config * fix: wxt config * fix: restore LF line endings for use-draggable.ts * chore: remove unused icon * fix: some low errors * fix: toggle button mount error * fix: extract constant * feat: move videoSubtitles options to new router * fix: migrate example error * refactor: remove Processor class and improve youtube segmenter algorithm * fix: fix test files * chore: remove claude settings * fix: remove useless logger * fix: improve code quality * fix: remove useless type * fix: move standard parser to single files * feat(subtitles): add zod validation for YouTube subtitle data * fix: replace setInterval * feat(subtitles): add error handling with dual display strategy * fix(subtitles): prevent cleanup() from clearing pending callbacks before resolve * fix: fix zod schema error * fix: type check error * refactor(subtitles): extract error classes for typed error handling * feat: improve scrolling parser * feat: add , to SENTENCE_END_PATTERN * feat: batch translate subtitles * feat: improve batch translate subtitles * feat: improve batch strategy * feat(subtitles): silent background translation with conditional state display * chore: add changeset * revert: revert subtitles atom * feat: improve parser and bolck logic * chore: remove useless props * fix: improve code quality * fix: remove blocks when youtube navigate * chore: remove useless console log --------- Co-authored-by: MengXi <[email protected]>
1 parent 4f0ce60 commit dadee30

File tree

18 files changed

+223
-53
lines changed

18 files changed

+223
-53
lines changed

.changeset/forty-ducks-find.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@read-frog/extension": patch
3+
---
4+
5+
feat: Improve the speed of subtitle translation through block translation

src/entrypoints/subtitles.content/atoms.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import type { StateData, SubtitlesFragment } from '@/utils/subtitles/types'
1+
import type { StateData, SubtitlesFragment, SubtitlesTranslationBlock } from '@/utils/subtitles/types'
22
import { atom, createStore } from 'jotai'
33

44
export const subtitlesStore = createStore()
55

6+
export const subtitlesTranslationBlocksAtom = atom<SubtitlesTranslationBlock[]>([])
7+
68
export const currentSubtitleAtom = atom<SubtitlesFragment | null>(null)
79

810
export const subtitlesStateAtom = atom<StateData | null>(null)

src/entrypoints/subtitles.content/subtitles-scheduler.ts

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { StateData, SubtitlesFragment, SubtitlesState } from '@/utils/subtitles/types'
2-
import { COMPLETED_STATE_HIDE_DELAY } from '@/utils/constants/subtitles'
32
import { currentSubtitleAtom, subtitlesStateAtom, subtitlesStore, subtitlesVisibleAtom } from './atoms'
43

54
export class SubtitlesScheduler {
@@ -11,8 +10,6 @@ export class SubtitlesScheduler {
1110
state: 'idle',
1211
}
1312

14-
private hideStateTimeout: NodeJS.Timeout | null = null
15-
1613
constructor({ videoElement }: { videoElement: HTMLVideoElement }) {
1714
this.videoElement = videoElement
1815
this.attachListeners()
@@ -28,15 +25,22 @@ export class SubtitlesScheduler {
2825
return
2926
}
3027

31-
this.subtitles.push(...subtitles)
32-
this.currentIndex = -1
33-
this.updateCurrentSubtitle()
28+
const existingStarts = new Set(this.subtitles.map(s => s.start))
29+
const newSubtitles = subtitles.filter(s => !existingStarts.has(s.start))
30+
31+
this.subtitles.push(...newSubtitles)
32+
this.subtitles.sort((a, b) => a.start - b.start)
33+
34+
this.updateSubtitles(this.videoElement.currentTime)
35+
}
36+
37+
getVideoElement(): HTMLVideoElement {
38+
return this.videoElement
3439
}
3540

3641
stop() {
3742
this.isActive = false
3843
this.detachListeners()
39-
this.clearHideStateTimeout()
4044
this.updateVisibility()
4145
}
4246

@@ -55,18 +59,7 @@ export class SubtitlesScheduler {
5559
state,
5660
message: data?.message,
5761
}
58-
this.clearHideStateTimeout()
59-
6062
this.updateState()
61-
62-
if (state === 'completed') {
63-
this.hideStateTimeout = setTimeout(
64-
() => {
65-
this.setState('idle')
66-
},
67-
COMPLETED_STATE_HIDE_DELAY,
68-
)
69-
}
7063
}
7164

7265
reset() {
@@ -126,11 +119,4 @@ export class SubtitlesScheduler {
126119
private updateVisibility() {
127120
subtitlesStore.set(subtitlesVisibleAtom, this.isActive)
128121
}
129-
130-
private clearHideStateTimeout() {
131-
if (this.hideStateTimeout) {
132-
clearTimeout(this.hideStateTimeout)
133-
this.hideStateTimeout = null
134-
}
135-
}
136122
}

src/entrypoints/subtitles.content/ui/state-message.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,6 @@ const STATE_CONFIG: Record<SubtitlesState, { color: string, getText: () => strin
2525
color: 'oklch(70% 0.19 250)',
2626
getText: () => i18n.t('subtitles.state.processing'),
2727
},
28-
completed: {
29-
color: 'oklch(70% 0.17 165)',
30-
getText: () => i18n.t('subtitles.state.completed'),
31-
},
3228
error: {
3329
color: 'oklch(63% 0.24 25)',
3430
getText: () => i18n.t('subtitles.state.error'),

src/entrypoints/subtitles.content/universal-adapter.ts

Lines changed: 88 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import type { PlatformConfig } from '@/entrypoints/subtitles.content/platforms'
22
import type { SubtitlesFetcher } from '@/utils/subtitles/fetchers/types'
3-
import type { SubtitlesFragment } from '@/utils/subtitles/types'
3+
import type { SubtitlesFragment, SubtitlesTranslationBlock } from '@/utils/subtitles/types'
44
import { i18n } from '#imports'
55
import { toast } from 'sonner'
66
import { HIDE_NATIVE_CAPTIONS_STYLE_ID, NAVIGATION_HANDLER_DELAY, TRANSLATE_BUTTON_CONTAINER_ID } from '@/utils/constants/subtitles'
77
import { waitForElement } from '@/utils/dom/wait-for-element'
88
import { ToastSubtitlesError } from '@/utils/subtitles/errors'
9+
import { createSubtitlesBlocks, findNextBlockToTranslate, updateBlockState } from '@/utils/subtitles/processor/block-strategy'
910
import { translateSubtitles } from '@/utils/subtitles/processor/translator'
11+
import { currentSubtitleAtom, subtitlesStore, subtitlesTranslationBlocksAtom } from './atoms'
1012
import { renderSubtitlesTranslateButton } from './renderer/render-translate-button'
1113
import { SubtitlesScheduler } from './subtitles-scheduler'
1214

@@ -39,12 +41,14 @@ export class UniversalVideoAdapter {
3941

4042
private resetSubtitlesData() {
4143
this.subtitlesScheduler?.reset()
44+
this.stopBlockMonitoring()
4245
this.originalSubtitles = []
4346
this.subtitlesFetcher.cleanup()
4447
}
4548

4649
private resetForNavigation() {
4750
this.destroyScheduler()
51+
this.stopBlockMonitoring()
4852
this.originalSubtitles = []
4953
this.cachedVideoId = null
5054
this.subtitlesFetcher.cleanup()
@@ -194,17 +198,96 @@ export class UniversalVideoAdapter {
194198
try {
195199
this.subtitlesScheduler?.setState('processing')
196200

197-
const translated = await translateSubtitles(this.originalSubtitles)
201+
const subtitlesBlocks = createSubtitlesBlocks(this.originalSubtitles)
202+
subtitlesStore.set(subtitlesTranslationBlocksAtom, subtitlesBlocks)
198203

199-
if (this.subtitlesScheduler) {
200-
this.subtitlesScheduler.supplementSubtitles(translated)
204+
const video = this.subtitlesScheduler?.getVideoElement()
205+
const currentTimeMs = (video?.currentTime ?? 0) * 1000
206+
const firstBlockToTranslate = findNextBlockToTranslate(subtitlesBlocks, currentTimeMs)
207+
208+
if (firstBlockToTranslate) {
209+
await this.translateSubtitlesBlock(firstBlockToTranslate)
201210
}
202211

203-
this.subtitlesScheduler?.setState('completed')
212+
this.startBlockMonitoring()
213+
this.subtitlesScheduler?.setState('idle')
214+
}
215+
catch (error) {
216+
const errorMessage = error instanceof Error ? error.message : String(error)
217+
this.subtitlesScheduler?.setState('error', { message: errorMessage })
218+
}
219+
}
220+
221+
private async translateSubtitlesBlock(batch: SubtitlesTranslationBlock) {
222+
const subtitlesBlocks = subtitlesStore.get(subtitlesTranslationBlocksAtom)
223+
subtitlesStore.set(subtitlesTranslationBlocksAtom, updateBlockState(subtitlesBlocks, batch.id, 'processing'))
224+
225+
const currentSubtitle = subtitlesStore.get(currentSubtitleAtom)
226+
if (!currentSubtitle) {
227+
this.subtitlesScheduler?.setState('processing')
228+
}
229+
230+
try {
231+
const translated = await translateSubtitles(batch.fragments)
232+
233+
const updatedBatches = subtitlesStore.get(subtitlesTranslationBlocksAtom)
234+
subtitlesStore.set(
235+
subtitlesTranslationBlocksAtom,
236+
updateBlockState(updatedBatches, batch.id, 'completed'),
237+
)
238+
239+
this.subtitlesScheduler?.supplementSubtitles(translated)
240+
this.subtitlesScheduler?.setState('idle')
204241
}
205242
catch (error) {
243+
const updatedBatches = subtitlesStore.get(subtitlesTranslationBlocksAtom)
244+
subtitlesStore.set(
245+
subtitlesTranslationBlocksAtom,
246+
updateBlockState(updatedBatches, batch.id, 'error'),
247+
)
248+
206249
const errorMessage = error instanceof Error ? error.message : String(error)
207250
this.subtitlesScheduler?.setState('error', { message: errorMessage })
208251
}
209252
}
253+
254+
private handleBlockCheck = () => {
255+
if (!this.subtitlesScheduler)
256+
return
257+
258+
const video = this.subtitlesScheduler.getVideoElement()
259+
260+
const currentTimeMs = video.currentTime * 1_000
261+
const blocks = subtitlesStore.get(subtitlesTranslationBlocksAtom)
262+
263+
if (blocks.some(b => b.state === 'processing'))
264+
return
265+
266+
const nextBlock = findNextBlockToTranslate(blocks, currentTimeMs)
267+
if (nextBlock) {
268+
void this.translateSubtitlesBlock(nextBlock)
269+
}
270+
}
271+
272+
private startBlockMonitoring() {
273+
if (!this.subtitlesScheduler)
274+
return
275+
276+
const video = this.subtitlesScheduler.getVideoElement()
277+
278+
video.addEventListener('seeked', this.handleBlockCheck)
279+
video.addEventListener('timeupdate', this.handleBlockCheck)
280+
}
281+
282+
private stopBlockMonitoring() {
283+
subtitlesStore.set(subtitlesTranslationBlocksAtom, [])
284+
285+
if (!this.subtitlesScheduler)
286+
return
287+
288+
const video = this.subtitlesScheduler.getVideoElement()
289+
290+
video.removeEventListener('seeked', this.handleBlockCheck)
291+
video.removeEventListener('timeupdate', this.handleBlockCheck)
292+
}
210293
}

src/locales/en.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -530,7 +530,6 @@ subtitles:
530530
fetchSuccess: Captions loaded
531531
fetchFailed: Failed to fetch captions
532532
processing: Translating captions (approx. 2 minutes)
533-
completed: Translation completed
534533
error: Error occurred
535534
errors:
536535
http429: 429 Too many requests to youtube subtitle API, please try again later

src/locales/ja.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,6 @@ subtitles:
526526
fetchSuccess: 字幕読み込み完了
527527
fetchFailed: 字幕取得失敗
528528
processing: 字幕翻訳中(約2分)
529-
completed: 翻訳完了
530529
error: エラーが発生しました
531530
errors:
532531
http429: 429 YouTube字幕APIへのリクエストが多すぎます。しばらくしてから再試行してください

src/locales/ko.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -527,7 +527,6 @@ subtitles:
527527
fetchSuccess: 자막 로드 완료
528528
fetchFailed: 자막 가져오기 실패
529529
processing: 자막 번역 중 (약 2분 소요)
530-
completed: 번역 완료
531530
error: 오류 발생
532531
errors:
533532
http429: 429 유튜브 자막 인터페이스 요청이 너무 많습니다. 잠시 후 다시 시도해 주세요

src/locales/ru.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -528,7 +528,6 @@ subtitles:
528528
fetchSuccess: Субтитры загружены
529529
fetchFailed: Не удалось получить субтитры
530530
processing: Перевод субтитров (примерно 2 минуты)
531-
completed: Перевод завершён
532531
error: Произошла ошибка
533532
errors:
534533
http429: 429 Слишком много запросов к API YouTube субтитров, пожалуйста, попробуйте снова позже

src/locales/tr.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -528,7 +528,6 @@ subtitles:
528528
fetchSuccess: Altyazılar yüklendi
529529
fetchFailed: Altyazılar alınamadı
530530
processing: Altyazılar çevriliyor (yaklaşık 2 dakika)
531-
completed: Çeviri tamamlandı
532531
error: Bir hata oluştu
533532
errors:
534533
http429: 429 YouTube altyazı API'sine çok fazla istek gönderildi. Lütfen biraz bekleyip tekrar deneyin.

0 commit comments

Comments
 (0)