fix(tts): improve segment grouping and sidebar selection for locator variants
Update segment manifest API to group segments by both index and locator attributes, ensuring distinct segment variants are not merged incorrectly. Adjust SegmentsSidebar logic to select the most relevant segment per index based on locator match and recency, improving sidebar accuracy when multiple locator variants exist for a segment.
This commit is contained in:
parent
4cb482c3f2
commit
45ae2f5000
2 changed files with 74 additions and 10 deletions
|
|
@ -90,6 +90,16 @@ function dedupeVariants(variants: Array<{ dedupeKey: string; variant: TTSSegment
|
|||
return Array.from(byKey.values());
|
||||
}
|
||||
|
||||
function locatorGroupKey(locator: TTSSegmentLocator | null): string {
|
||||
if (!locator) return 'none';
|
||||
const page = typeof locator.page === 'number' && Number.isFinite(locator.page)
|
||||
? String(Math.floor(locator.page))
|
||||
: '';
|
||||
const location = typeof locator.location === 'string' ? locator.location : '';
|
||||
const readerType = locator.readerType || '';
|
||||
return `p:${page}|l:${location}|r:${readerType}`;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const documentIdRaw = request.nextUrl.searchParams.get('documentId');
|
||||
|
|
@ -131,21 +141,24 @@ export async function GET(request: NextRequest) {
|
|||
updatedAt: number | null;
|
||||
}>;
|
||||
|
||||
const grouped = new Map<number, Omit<TTSSegmentRow, 'variants'> & {
|
||||
const grouped = new Map<string, Omit<TTSSegmentRow, 'variants'> & {
|
||||
variants: Array<{ dedupeKey: string; variant: TTSSegmentVariant }>;
|
||||
}>();
|
||||
|
||||
for (const row of rows) {
|
||||
let entry = grouped.get(row.segmentIndex);
|
||||
const locator = parseLocator(row.locatorJson);
|
||||
const groupKey = `${row.segmentIndex}|${locatorGroupKey(locator)}`;
|
||||
|
||||
let entry = grouped.get(groupKey);
|
||||
if (!entry) {
|
||||
entry = {
|
||||
segmentIndex: row.segmentIndex,
|
||||
locator: parseLocator(row.locatorJson),
|
||||
locator,
|
||||
variants: [],
|
||||
};
|
||||
grouped.set(row.segmentIndex, entry);
|
||||
grouped.set(groupKey, entry);
|
||||
} else if (!entry.locator) {
|
||||
entry.locator = parseLocator(row.locatorJson);
|
||||
entry.locator = locator;
|
||||
}
|
||||
|
||||
let alignmentWordCount = 0;
|
||||
|
|
@ -189,7 +202,15 @@ export async function GET(request: NextRequest) {
|
|||
locator: segment.locator,
|
||||
variants: dedupeVariants(segment.variants),
|
||||
}))
|
||||
.sort((a, b) => a.segmentIndex - b.segmentIndex);
|
||||
.sort((a, b) => {
|
||||
if (a.segmentIndex !== b.segmentIndex) return a.segmentIndex - b.segmentIndex;
|
||||
const aPage = typeof a.locator?.page === 'number' ? a.locator.page : Number.MAX_SAFE_INTEGER;
|
||||
const bPage = typeof b.locator?.page === 'number' ? b.locator.page : Number.MAX_SAFE_INTEGER;
|
||||
if (aPage !== bPage) return aPage - bPage;
|
||||
const aLoc = a.locator?.location || '';
|
||||
const bLoc = b.locator?.location || '';
|
||||
return aLoc.localeCompare(bLoc);
|
||||
});
|
||||
const response: TTSSegmentsManifestResponse = { documentId, segments };
|
||||
return NextResponse.json(response);
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { useConfig } from '@/contexts/ConfigContext';
|
|||
import { RefreshIcon, InfoIcon } from '@/components/icons/Icons';
|
||||
import { ReaderSidebarShell } from '@/components/reader/ReaderSidebarShell';
|
||||
import type {
|
||||
TTSSegmentLocator,
|
||||
TTSSegmentRow,
|
||||
TTSSegmentSettings,
|
||||
TTSSegmentVariant,
|
||||
|
|
@ -75,10 +76,34 @@ function statusColor(status: TTSSegmentVariant['status']): string {
|
|||
return 'bg-muted';
|
||||
}
|
||||
|
||||
function locatorMatchesCurrent(
|
||||
locator: TTSSegmentLocator | null,
|
||||
currentLocation: string | number,
|
||||
currentPageNumber: number,
|
||||
): boolean {
|
||||
if (!locator) return false;
|
||||
if (typeof locator.location === 'string' && locator.location.length > 0) {
|
||||
return String(locator.location) === String(currentLocation);
|
||||
}
|
||||
if (typeof locator.page === 'number' && Number.isFinite(locator.page)) {
|
||||
return Math.floor(locator.page) === Math.floor(Number(currentPageNumber || 1));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function latestUpdatedAt(row: TTSSegmentRow): number {
|
||||
return row.variants.reduce((max, variant) => {
|
||||
const updated = typeof variant.updatedAt === 'number' ? variant.updatedAt : 0;
|
||||
return Math.max(max, updated);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
export function SegmentsSidebar({ isOpen, setIsOpen, documentId }: SegmentsSidebarProps) {
|
||||
const {
|
||||
sentences,
|
||||
currentSentenceIndex,
|
||||
currDocPage,
|
||||
currDocPageNumber,
|
||||
isPlaying,
|
||||
stopAndPlayFromIndex,
|
||||
} = useTTS();
|
||||
|
|
@ -170,10 +195,28 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId }: SegmentsSideb
|
|||
|
||||
const segmentsByIndex = useMemo(() => {
|
||||
if (state.kind !== 'ready') return new Map<number, TTSSegmentRow>();
|
||||
const map = new Map<number, TTSSegmentRow>();
|
||||
for (const row of state.data) map.set(row.segmentIndex, row);
|
||||
return map;
|
||||
}, [state]);
|
||||
const map = new Map<number, { row: TTSSegmentRow; score: number; updatedAt: number }>();
|
||||
for (const row of state.data) {
|
||||
const isCurrent = locatorMatchesCurrent(row.locator, currDocPage, currDocPageNumber);
|
||||
const score = isCurrent ? 2 : row.locator ? 0 : 1;
|
||||
if (score === 0) continue;
|
||||
|
||||
const candidateUpdatedAt = latestUpdatedAt(row);
|
||||
const existing = map.get(row.segmentIndex);
|
||||
if (!existing) {
|
||||
map.set(row.segmentIndex, { row, score, updatedAt: candidateUpdatedAt });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (score > existing.score || (score === existing.score && candidateUpdatedAt >= existing.updatedAt)) {
|
||||
map.set(row.segmentIndex, { row, score, updatedAt: candidateUpdatedAt });
|
||||
}
|
||||
}
|
||||
|
||||
const selected = new Map<number, TTSSegmentRow>();
|
||||
for (const [idx, entry] of map) selected.set(idx, entry.row);
|
||||
return selected;
|
||||
}, [state, currDocPage, currDocPageNumber]);
|
||||
|
||||
const indicesToRender = useMemo(() => {
|
||||
const indices = new Set<number>();
|
||||
|
|
|
|||
Loading…
Reference in a new issue