refactor(tts): extend segment prefetching to support source unit arrays and enhance cache key granularity
Update TTS segment prefetching logic to handle arrays of CanonicalTtsSourceUnit for upcoming and next locations, enabling more precise TTS segment mapping. Expand cache key construction to include providerType and instructions, improving cache differentiation for TTS requests. Adjust related types and usages to support richer segment metadata propagation.
This commit is contained in:
parent
9d1f9d5707
commit
bb3eb40966
9 changed files with 69 additions and 60 deletions
|
|
@ -337,7 +337,6 @@ export default function PDFViewerPage() {
|
|||
parseStatus,
|
||||
parsedOverlayEnabled,
|
||||
skipBlockKinds: documentSettings.pdf?.skipBlockKinds ?? [],
|
||||
chaptersFromSections: documentSettings.pdf?.chaptersFromSections ?? true,
|
||||
onToggleOverlay: (enabled) => setParsedOverlayEnabled(enabled),
|
||||
onToggleSkipKind: (kind, enabled) => {
|
||||
const current = new Set(documentSettings.pdf?.skipBlockKinds ?? []);
|
||||
|
|
@ -347,20 +346,8 @@ export default function PDFViewerPage() {
|
|||
...documentSettings,
|
||||
schemaVersion: 1,
|
||||
pdf: {
|
||||
...(documentSettings.pdf ?? { chaptersFromSections: true }),
|
||||
...(documentSettings.pdf ?? {}),
|
||||
skipBlockKinds: Array.from(current),
|
||||
chaptersFromSections: documentSettings.pdf?.chaptersFromSections ?? true,
|
||||
},
|
||||
});
|
||||
},
|
||||
onToggleChaptersFromSections: (enabled) => {
|
||||
void updateDocumentSettings({
|
||||
...documentSettings,
|
||||
schemaVersion: 1,
|
||||
pdf: {
|
||||
...(documentSettings.pdf ?? { skipBlockKinds: [] }),
|
||||
skipBlockKinds: documentSettings.pdf?.skipBlockKinds ?? [],
|
||||
chaptersFromSections: enabled,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ export function usePdfDocument(): PdfDocumentState {
|
|||
text: block.text,
|
||||
locator: {
|
||||
readerType: 'pdf',
|
||||
page: block.fragments[0]?.page ?? pageNum,
|
||||
page: pageNum,
|
||||
blockId: block.id,
|
||||
} as TTSSegmentLocator,
|
||||
}))
|
||||
|
|
@ -328,11 +328,13 @@ export function usePdfDocument(): PdfDocumentState {
|
|||
...upcomingPageNumbers.map((pageNum) => getPageText(pageNum, true)),
|
||||
]);
|
||||
const nextText = upcomingTexts[0];
|
||||
const nextSourceUnits = nextPageNumber ? sourceUnitsFromParsedPage(nextPageNumber) : [];
|
||||
const additionalUpcoming = upcomingPageNumbers
|
||||
.slice(1)
|
||||
.map((pageNum, idx) => ({
|
||||
location: pageNum,
|
||||
text: upcomingTexts[idx + 1] || '',
|
||||
sourceUnits: sourceUnitsFromParsedPage(pageNum),
|
||||
}))
|
||||
.filter((item) => item.text.trim().length > 0);
|
||||
|
||||
|
|
@ -351,6 +353,7 @@ export function usePdfDocument(): PdfDocumentState {
|
|||
previousText: prevText,
|
||||
nextLocation: nextPageNumber,
|
||||
nextText: nextText,
|
||||
nextSourceUnits,
|
||||
upcomingLocations: additionalUpcoming,
|
||||
...(sourceUnits.length > 0 ? { sourceUnits } : {}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -103,10 +103,8 @@ export function DocumentSettings({ isOpen, setIsOpen, epub, html, pdf }: {
|
|||
parseStatus: PdfParseStatus | null;
|
||||
parsedOverlayEnabled: boolean;
|
||||
skipBlockKinds: ParsedPdfBlockKind[];
|
||||
chaptersFromSections: boolean;
|
||||
onToggleOverlay: (enabled: boolean) => void;
|
||||
onToggleSkipKind: (kind: ParsedPdfBlockKind, enabled: boolean) => void;
|
||||
onToggleChaptersFromSections: (enabled: boolean) => void;
|
||||
onForceReparse: () => void;
|
||||
}
|
||||
}) {
|
||||
|
|
@ -204,13 +202,6 @@ export function DocumentSettings({ isOpen, setIsOpen, epub, html, pdf }: {
|
|||
onChange={(checked) => updateConfigKey('pdfWordHighlightEnabled', checked)}
|
||||
variant="flat"
|
||||
/>
|
||||
<ToggleRow
|
||||
label="Use detected sections as chapters"
|
||||
description="Build audiobook chapters from section headers."
|
||||
checked={pdf.chaptersFromSections}
|
||||
onChange={pdf.onToggleChaptersFromSections}
|
||||
variant="flat"
|
||||
/>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -249,6 +249,8 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId, epubBookRef }:
|
|||
const segmentsQuery = useInfiniteQuery({
|
||||
queryKey: segmentsQueryKey,
|
||||
enabled: isOpen && !!documentId,
|
||||
refetchInterval: isOpen ? 2500 : false,
|
||||
refetchIntervalInBackground: false,
|
||||
initialPageParam: null as string | null,
|
||||
queryFn: async ({ pageParam, signal }) => {
|
||||
if (!documentId) {
|
||||
|
|
|
|||
|
|
@ -163,8 +163,9 @@ interface SetTextOptions {
|
|||
previousLocation?: TTSLocation;
|
||||
nextLocation?: TTSLocation;
|
||||
nextText?: string;
|
||||
nextSourceUnits?: CanonicalTtsSourceUnit[];
|
||||
previousText?: string;
|
||||
upcomingLocations?: Array<{ location: TTSLocation; text: string }>;
|
||||
upcomingLocations?: Array<{ location: TTSLocation; text: string; sourceUnits?: CanonicalTtsSourceUnit[] }>;
|
||||
}
|
||||
|
||||
type TTSSegmentPlaybackSource = {
|
||||
|
|
@ -245,12 +246,16 @@ const buildCacheKey = (
|
|||
speed: number,
|
||||
provider: string,
|
||||
model: string,
|
||||
providerType: string,
|
||||
instructions: string,
|
||||
) => {
|
||||
return [
|
||||
`provider=${provider || ''}`,
|
||||
`providerType=${providerType || ''}`,
|
||||
`model=${model || ''}`,
|
||||
`voice=${voice || ''}`,
|
||||
`speed=${Number.isFinite(speed) ? speed : ''}`,
|
||||
`instructions=${instructions || ''}`,
|
||||
`text=${sentence}`,
|
||||
].join('|');
|
||||
};
|
||||
|
|
@ -263,10 +268,12 @@ const buildScopedSegmentCacheKey = (
|
|||
speed: number,
|
||||
provider: string,
|
||||
model: string,
|
||||
providerType: string,
|
||||
instructions: string,
|
||||
segmentKey?: string | null,
|
||||
) => {
|
||||
return [
|
||||
buildCacheKey(sentence, voice, speed, provider, model),
|
||||
buildCacheKey(sentence, voice, speed, provider, model, providerType, instructions),
|
||||
`segmentKey=${segmentKey || ''}`,
|
||||
`locator=${segmentKey ? '' : buildLocatorRequestKey(locator)}`,
|
||||
`segmentIndex=${segmentKey ? '' : segmentIndex}`,
|
||||
|
|
@ -306,9 +313,11 @@ const buildSegmentRequestKey = (
|
|||
segmentIndex: number,
|
||||
sentence: string,
|
||||
segmentKey?: string | null,
|
||||
): string => segmentKey
|
||||
? `${segmentKey}::${sentence}`
|
||||
: `${buildLocatorRequestKey(locator)}::${segmentIndex}::${sentence}`;
|
||||
): string => {
|
||||
return segmentKey
|
||||
? `${segmentKey}::${sentence}`
|
||||
: `${buildLocatorRequestKey(locator)}::${segmentIndex}::${sentence}`;
|
||||
};
|
||||
|
||||
const resolveJumpIndex = (input: JumpResolutionInput): JumpResolution => {
|
||||
if (input.newSentenceCount <= 0) {
|
||||
|
|
@ -1172,29 +1181,48 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
|||
|
||||
plannedSegmentsByLocationRef.current.clear();
|
||||
pendingNextLocationRef.current = normalizedOptions.nextLocation;
|
||||
const pendingPrefetches: Array<CanonicalTtsSourceUnit & { location: TTSLocation }> = [];
|
||||
const pendingPrefetches: Array<{
|
||||
location: TTSLocation;
|
||||
sourceUnits: CanonicalTtsSourceUnit[];
|
||||
}> = [];
|
||||
if (normalizedOptions.nextLocation !== undefined && normalizedOptions.nextText?.trim()) {
|
||||
pendingPrefetches.push({
|
||||
sourceKey: sourceKeyForLocation(normalizedOptions.nextLocation, currDocPage),
|
||||
location: normalizedOptions.nextLocation,
|
||||
text: normalizedOptions.nextText,
|
||||
locator: locatorForLocation(normalizedOptions.nextLocation, activeReaderType),
|
||||
});
|
||||
const provided = normalizedOptions.nextSourceUnits?.filter((unit) => unit.text.trim().length > 0) ?? [];
|
||||
pendingPrefetches.push(provided.length > 0
|
||||
? {
|
||||
location: normalizedOptions.nextLocation,
|
||||
sourceUnits: provided,
|
||||
}
|
||||
: {
|
||||
location: normalizedOptions.nextLocation,
|
||||
sourceUnits: [{
|
||||
sourceKey: sourceKeyForLocation(normalizedOptions.nextLocation, currDocPage),
|
||||
text: normalizedOptions.nextText,
|
||||
locator: locatorForLocation(normalizedOptions.nextLocation, activeReaderType),
|
||||
}],
|
||||
});
|
||||
}
|
||||
if (Array.isArray(normalizedOptions.upcomingLocations)) {
|
||||
for (const item of normalizedOptions.upcomingLocations) {
|
||||
if (item.location === undefined || !item.text?.trim()) continue;
|
||||
pendingPrefetches.push({
|
||||
sourceKey: sourceKeyForLocation(item.location, currDocPage),
|
||||
location: item.location,
|
||||
text: item.text,
|
||||
locator: locatorForLocation(item.location, activeReaderType),
|
||||
});
|
||||
const provided = item.sourceUnits?.filter((unit) => unit.text.trim().length > 0) ?? [];
|
||||
pendingPrefetches.push(provided.length > 0
|
||||
? {
|
||||
location: item.location,
|
||||
sourceUnits: provided,
|
||||
}
|
||||
: {
|
||||
location: item.location,
|
||||
sourceUnits: [{
|
||||
sourceKey: sourceKeyForLocation(item.location, currDocPage),
|
||||
text: item.text,
|
||||
locator: locatorForLocation(item.location, activeReaderType),
|
||||
}],
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const item of pendingPrefetches) {
|
||||
if (smartSentenceSplitting) {
|
||||
sourceUnits.push(item);
|
||||
sourceUnits.push(...item.sourceUnits);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1216,9 +1244,10 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
|||
const newSentences = currentSegments.map((segment) => segment.text);
|
||||
|
||||
for (const item of pendingPrefetches) {
|
||||
const sourceKeys = new Set(item.sourceUnits.map((unit) => unit.sourceKey));
|
||||
const planned = smartSentenceSplitting
|
||||
? plan.segments.filter((segment) => segment.ownerSourceKey === item.sourceKey)
|
||||
: planCanonicalTtsSegments([item], {
|
||||
? plan.segments.filter((segment) => sourceKeys.has(segment.ownerSourceKey))
|
||||
: planCanonicalTtsSegments(item.sourceUnits, {
|
||||
readerType: activeReaderType,
|
||||
maxBlockLength: ttsSegmentMaxBlockLength,
|
||||
keyPrefix: buildSegmentKeyPrefix(documentId, activeReaderType),
|
||||
|
|
@ -1554,6 +1583,8 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
|||
effectiveNativeSpeed,
|
||||
configProviderRef,
|
||||
ttsModel,
|
||||
configProviderType,
|
||||
providerModelPolicy.supportsInstructions ? ttsInstructions : '',
|
||||
segmentKey,
|
||||
);
|
||||
|
||||
|
|
@ -2216,6 +2247,8 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
|||
effectiveNativeSpeed,
|
||||
configProviderRef,
|
||||
ttsModel,
|
||||
configProviderType,
|
||||
providerModelPolicy.supportsInstructions ? ttsInstructions : '',
|
||||
playbackSegment?.key,
|
||||
);
|
||||
const cachedAlignment = sentenceAlignmentCacheRef.current.get(alignmentKey);
|
||||
|
|
@ -2346,6 +2379,8 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
|||
effectiveNativeSpeed,
|
||||
configProviderRef,
|
||||
ttsModel,
|
||||
configProviderType,
|
||||
providerModelPolicy.supportsInstructions ? ttsInstructions : '',
|
||||
nextSegment?.key,
|
||||
);
|
||||
if (segmentManifestCacheRef.current.has(cacheKey)) {
|
||||
|
|
@ -2477,6 +2512,8 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
|||
effectiveNativeSpeed,
|
||||
configProviderRef,
|
||||
ttsModel,
|
||||
configProviderType,
|
||||
providerModelPolicy.supportsInstructions ? ttsInstructions : '',
|
||||
segment.key,
|
||||
);
|
||||
if (seenCandidates.has(requestKey)) continue;
|
||||
|
|
@ -2615,6 +2652,8 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
|||
effectiveNativeSpeed,
|
||||
configProviderRef,
|
||||
ttsModel,
|
||||
configProviderType,
|
||||
providerModelPolicy.supportsInstructions ? ttsInstructions : '',
|
||||
plannedSegment?.key,
|
||||
);
|
||||
const requestKey = buildSegmentRequestKey(locator, sentenceIndex, sentence, plannedSegment?.key);
|
||||
|
|
@ -2644,6 +2683,8 @@ export function TTSProvider({ children }: { children: ReactNode }): ReactElement
|
|||
effectiveNativeSpeed,
|
||||
configProviderRef,
|
||||
ttsModel,
|
||||
configProviderType,
|
||||
providerModelPolicy.supportsInstructions ? ttsInstructions : '',
|
||||
segment.key,
|
||||
);
|
||||
const requestKey = buildSegmentRequestKey(segmentLocator, index, sentence, segment.key);
|
||||
|
|
|
|||
|
|
@ -41,12 +41,6 @@ function prepareParsedChapters({
|
|||
.filter((block) => !skip.has(block.kind));
|
||||
if (!allBlocks.length) return [];
|
||||
|
||||
const chaptersFromSections = settings.pdf?.chaptersFromSections ?? true;
|
||||
if (!chaptersFromSections) {
|
||||
const text = chapterTextFromBlocks(allBlocks, smartSentenceSplitting, maxBlockLength);
|
||||
return text ? [{ index: 0, title: 'Document', text }] : [];
|
||||
}
|
||||
|
||||
const chapters: PreparedAudiobookChapter[] = [];
|
||||
let currentTitle = 'Introduction';
|
||||
let currentBlocks: ParsedPdfBlock[] = [];
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ export function mergeDocumentSettings(
|
|||
schemaVersion: 1,
|
||||
pdf: {
|
||||
skipBlockKinds: [...(defaults.pdf?.skipBlockKinds ?? [])],
|
||||
chaptersFromSections: defaults.pdf?.chaptersFromSections ?? true,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -38,10 +37,6 @@ export function mergeDocumentSettings(
|
|||
schemaVersion: 1,
|
||||
pdf: {
|
||||
skipBlockKinds: normalizeSkipKinds(pdfRec.skipBlockKinds),
|
||||
chaptersFromSections:
|
||||
typeof pdfRec.chaptersFromSections === 'boolean'
|
||||
? pdfRec.chaptersFromSections
|
||||
: (base.pdf?.chaptersFromSections ?? true),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ export interface DocumentSettings {
|
|||
schemaVersion: 1;
|
||||
pdf?: {
|
||||
skipBlockKinds: ParsedPdfBlockKind[];
|
||||
chaptersFromSections: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -12,6 +11,5 @@ export const DEFAULT_DOCUMENT_SETTINGS: DocumentSettings = {
|
|||
schemaVersion: 1,
|
||||
pdf: {
|
||||
skipBlockKinds: ['header', 'footer', 'footnote', 'vision_footnote'],
|
||||
chaptersFromSections: true,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ test.describe('pdf audiobook adapter', () => {
|
|||
schemaVersion: 1,
|
||||
pdf: {
|
||||
skipBlockKinds: ['header'],
|
||||
chaptersFromSections: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -120,7 +119,6 @@ test.describe('pdf audiobook adapter', () => {
|
|||
schemaVersion: 1,
|
||||
pdf: {
|
||||
skipBlockKinds: [],
|
||||
chaptersFromSections: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue