feat: remaining time in TTS mode (#3300)
* feat: add test * chore: update test * feat: add translations * feat: add tts time * feat: add tts time components * feat: update test * feat: fixes and update ui components * refactor: revert translations and ui
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { estimateTTSTime } from '@/utils/ttsTime';
|
||||
import { BookProgress } from '@/types/book';
|
||||
|
||||
const createProgress = (values: {
|
||||
sectionMin: number;
|
||||
totalMin: number;
|
||||
pageCurrent: number;
|
||||
pageTotal: number;
|
||||
}) => {
|
||||
return {
|
||||
timeinfo: {
|
||||
section: values.sectionMin,
|
||||
total: values.totalMin,
|
||||
},
|
||||
pageinfo: {
|
||||
current: values.pageCurrent,
|
||||
total: values.pageTotal,
|
||||
},
|
||||
} as BookProgress;
|
||||
};
|
||||
|
||||
describe('estimateTTSTime', () => {
|
||||
it('estimates chapter and book remaining time at 1x', () => {
|
||||
const progress = createProgress({
|
||||
sectionMin: 10,
|
||||
totalMin: 60,
|
||||
pageCurrent: 49,
|
||||
pageTotal: 100,
|
||||
});
|
||||
|
||||
const result = estimateTTSTime(progress, 1, 1_700_000_000_000);
|
||||
|
||||
expect(result.chapterRemainingSec).toBe(600);
|
||||
expect(result.bookRemainingSec).toBe(3600);
|
||||
expect(result.finishAtTimestamp).toBe(1_700_003_600_000);
|
||||
});
|
||||
|
||||
it('scales estimates by playback rate', () => {
|
||||
const progress = createProgress({
|
||||
sectionMin: 12,
|
||||
totalMin: 90,
|
||||
pageCurrent: 44,
|
||||
pageTotal: 90,
|
||||
});
|
||||
|
||||
const result = estimateTTSTime(progress, 1.5, 1000);
|
||||
|
||||
expect(result.chapterRemainingSec).toBe(480);
|
||||
expect(result.bookRemainingSec).toBe(3600);
|
||||
expect(result.finishAtTimestamp).toBe(3_601_000);
|
||||
});
|
||||
|
||||
it('falls back safely for missing progress', () => {
|
||||
const result = estimateTTSTime(null, 2, 1000);
|
||||
|
||||
expect(result.chapterRemainingSec).toBeNull();
|
||||
expect(result.bookRemainingSec).toBeNull();
|
||||
expect(result.finishAtTimestamp).toBeNull();
|
||||
});
|
||||
|
||||
it('uses book remaining to compute finish time', () => {
|
||||
const progress = createProgress({
|
||||
sectionMin: 1,
|
||||
totalMin: 2,
|
||||
pageCurrent: 99,
|
||||
pageTotal: 100,
|
||||
});
|
||||
|
||||
const result = estimateTTSTime(progress, 1, 1000);
|
||||
|
||||
expect(result.bookRemainingSec).toBe(120);
|
||||
expect(result.finishAtTimestamp).toBe(121000);
|
||||
});
|
||||
});
|
||||
@@ -16,6 +16,7 @@ import { throttle } from '@/utils/throttle';
|
||||
import { isCfiInLocation } from '@/utils/cfi';
|
||||
import { getLocale } from '@/utils/misc';
|
||||
import { invokeUseBackgroundAudio } from '@/utils/bridge';
|
||||
import { estimateTTSTime } from '@/utils/ttsTime';
|
||||
import { useTTSMediaSession } from './useTTSMediaSession';
|
||||
|
||||
interface UseTTSControlProps {
|
||||
@@ -224,6 +225,11 @@ export const useTTSControl = ({ bookKey, onRequestHidePanel }: UseTTSControlProp
|
||||
};
|
||||
|
||||
const viewSettings = getViewSettings(bookKey);
|
||||
const ttsTime = useMemo(() => {
|
||||
const rate = viewSettings?.ttsRate ?? 1;
|
||||
return estimateTTSTime(progress, rate);
|
||||
}, [progress, viewSettings?.ttsRate]);
|
||||
|
||||
const getTTSTargetLang = useCallback((): string | null => {
|
||||
const vs = getViewSettings(bookKey);
|
||||
const ttsReadAloudText = vs?.ttsReadAloudText;
|
||||
@@ -641,6 +647,9 @@ export const useTTSControl = ({ bookKey, onRequestHidePanel }: UseTTSControlProp
|
||||
showBackToCurrentTTSLocation,
|
||||
timeoutOption,
|
||||
timeoutTimestamp,
|
||||
chapterRemainingSec: ttsTime.chapterRemainingSec,
|
||||
bookRemainingSec: ttsTime.bookRemainingSec,
|
||||
finishAtTimestamp: ttsTime.finishAtTimestamp,
|
||||
handleTogglePlay,
|
||||
handleBackward,
|
||||
handleForward,
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { BookProgress } from '@/types/book';
|
||||
|
||||
const toSeconds = (minutes?: number) => {
|
||||
if (typeof minutes !== 'number' || !Number.isFinite(minutes) || minutes < 0) return null;
|
||||
return Math.round(minutes * 60);
|
||||
};
|
||||
|
||||
const getSafeRate = (rate?: number) => {
|
||||
if (typeof rate !== 'number' || !Number.isFinite(rate) || rate <= 0) return 1;
|
||||
return rate;
|
||||
};
|
||||
|
||||
const applyRate = (seconds: number | null, rate: number) => {
|
||||
if (seconds === null) return null;
|
||||
return Math.max(0, Math.round(seconds / rate));
|
||||
};
|
||||
|
||||
export type TTSTimeEstimate = {
|
||||
chapterRemainingSec: number | null;
|
||||
bookRemainingSec: number | null;
|
||||
finishAtTimestamp: number | null;
|
||||
};
|
||||
|
||||
export const estimateTTSTime = (
|
||||
progress: BookProgress | null,
|
||||
rate?: number,
|
||||
now = Date.now(),
|
||||
): TTSTimeEstimate => {
|
||||
const safeRate = getSafeRate(rate);
|
||||
const chapterRemainingBaseSec = toSeconds(progress?.timeinfo?.section);
|
||||
const bookRemainingBaseSec = toSeconds(progress?.timeinfo?.total);
|
||||
|
||||
const chapterRemainingSec = applyRate(chapterRemainingBaseSec, safeRate);
|
||||
const bookRemainingSec = applyRate(bookRemainingBaseSec, safeRate);
|
||||
|
||||
const finishAtTimestamp =
|
||||
bookRemainingSec !== null && bookRemainingSec > 0 ? now + bookRemainingSec * 1000 : null;
|
||||
|
||||
return {
|
||||
chapterRemainingSec,
|
||||
bookRemainingSec,
|
||||
finishAtTimestamp,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user