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());
|
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) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const documentIdRaw = request.nextUrl.searchParams.get('documentId');
|
const documentIdRaw = request.nextUrl.searchParams.get('documentId');
|
||||||
|
|
@ -131,21 +141,24 @@ export async function GET(request: NextRequest) {
|
||||||
updatedAt: number | null;
|
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 }>;
|
variants: Array<{ dedupeKey: string; variant: TTSSegmentVariant }>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
for (const row of rows) {
|
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) {
|
if (!entry) {
|
||||||
entry = {
|
entry = {
|
||||||
segmentIndex: row.segmentIndex,
|
segmentIndex: row.segmentIndex,
|
||||||
locator: parseLocator(row.locatorJson),
|
locator,
|
||||||
variants: [],
|
variants: [],
|
||||||
};
|
};
|
||||||
grouped.set(row.segmentIndex, entry);
|
grouped.set(groupKey, entry);
|
||||||
} else if (!entry.locator) {
|
} else if (!entry.locator) {
|
||||||
entry.locator = parseLocator(row.locatorJson);
|
entry.locator = locator;
|
||||||
}
|
}
|
||||||
|
|
||||||
let alignmentWordCount = 0;
|
let alignmentWordCount = 0;
|
||||||
|
|
@ -189,7 +202,15 @@ export async function GET(request: NextRequest) {
|
||||||
locator: segment.locator,
|
locator: segment.locator,
|
||||||
variants: dedupeVariants(segment.variants),
|
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 };
|
const response: TTSSegmentsManifestResponse = { documentId, segments };
|
||||||
return NextResponse.json(response);
|
return NextResponse.json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { useConfig } from '@/contexts/ConfigContext';
|
||||||
import { RefreshIcon, InfoIcon } from '@/components/icons/Icons';
|
import { RefreshIcon, InfoIcon } from '@/components/icons/Icons';
|
||||||
import { ReaderSidebarShell } from '@/components/reader/ReaderSidebarShell';
|
import { ReaderSidebarShell } from '@/components/reader/ReaderSidebarShell';
|
||||||
import type {
|
import type {
|
||||||
|
TTSSegmentLocator,
|
||||||
TTSSegmentRow,
|
TTSSegmentRow,
|
||||||
TTSSegmentSettings,
|
TTSSegmentSettings,
|
||||||
TTSSegmentVariant,
|
TTSSegmentVariant,
|
||||||
|
|
@ -75,10 +76,34 @@ function statusColor(status: TTSSegmentVariant['status']): string {
|
||||||
return 'bg-muted';
|
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) {
|
export function SegmentsSidebar({ isOpen, setIsOpen, documentId }: SegmentsSidebarProps) {
|
||||||
const {
|
const {
|
||||||
sentences,
|
sentences,
|
||||||
currentSentenceIndex,
|
currentSentenceIndex,
|
||||||
|
currDocPage,
|
||||||
|
currDocPageNumber,
|
||||||
isPlaying,
|
isPlaying,
|
||||||
stopAndPlayFromIndex,
|
stopAndPlayFromIndex,
|
||||||
} = useTTS();
|
} = useTTS();
|
||||||
|
|
@ -170,10 +195,28 @@ export function SegmentsSidebar({ isOpen, setIsOpen, documentId }: SegmentsSideb
|
||||||
|
|
||||||
const segmentsByIndex = useMemo(() => {
|
const segmentsByIndex = useMemo(() => {
|
||||||
if (state.kind !== 'ready') return new Map<number, TTSSegmentRow>();
|
if (state.kind !== 'ready') return new Map<number, TTSSegmentRow>();
|
||||||
const map = new Map<number, TTSSegmentRow>();
|
const map = new Map<number, { row: TTSSegmentRow; score: number; updatedAt: number }>();
|
||||||
for (const row of state.data) map.set(row.segmentIndex, row);
|
for (const row of state.data) {
|
||||||
return map;
|
const isCurrent = locatorMatchesCurrent(row.locator, currDocPage, currDocPageNumber);
|
||||||
}, [state]);
|
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 indicesToRender = useMemo(() => {
|
||||||
const indices = new Set<number>();
|
const indices = new Set<number>();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue