import type { StoredParagraph, StoredSentence, } from "../infrastructure/storage/lectureStore"; export function normalizeSelectionText(text: string): string { return text .replace(/[\u0000-\u001f\f]/g, " ") .replace(/\s+/g, " ") .trim() .toLowerCase(); } export interface PreparedSentenceMatch { paragraph: StoredParagraph; sentence: StoredSentence; } function overlapScore(a: string, b: string): number { const na = normalizeSelectionText(a); const nb = normalizeSelectionText(b); if (!na || !nb) return 0; if (na === nb) return 100; if (na.includes(nb) || nb.includes(na)) { const shorter = Math.min(na.length, nb.length); const longer = Math.max(na.length, nb.length); return Math.round((shorter / longer) * 90); } const aWords = new Set(na.split(/\s+/).filter((w) => w.length > 2)); const bWords = nb.split(/\s+/).filter((w) => w.length > 2); if (!aWords.size || !bWords.length) return 0; let shared = 0; for (const w of bWords) { if (aWords.has(w)) shared += 1; } return Math.round((shared / bWords.length) * 70); } export function findMatchingPreparedSentence( paragraphs: StoredParagraph[] | undefined, selectedText: string, options?: { requireTts?: boolean }, ): PreparedSentenceMatch | undefined { if (!paragraphs?.length || !selectedText.trim()) return undefined; const requireTts = options?.requireTts !== false; let best: PreparedSentenceMatch | undefined; let bestScore = 0; for (const paragraph of paragraphs) { if (paragraph.genStatus !== "ready") continue; for (const sentence of paragraph.sentences ?? []) { if (requireTts && sentence.ttsZhStatus !== "ready") continue; const score = overlapScore(selectedText, sentence.text); if (score > bestScore) { bestScore = score; best = { paragraph, sentence }; } } if (!paragraph.sentences?.length) { const score = overlapScore(selectedText, paragraph.originalText); if (score > bestScore && (!requireTts || paragraph.ttsZhStatus === "ready")) { bestScore = score; best = { paragraph, sentence: { id: `${paragraph.id}-whole`, orderIndex: 0, text: paragraph.originalText, translation: paragraph.translation, page: paragraph.page, searchText: paragraph.searchText, audioPathEn: paragraph.audioPathEn, audioPathZh: paragraph.audioPathZh, ttsEnStatus: paragraph.ttsEnStatus, ttsZhStatus: paragraph.ttsZhStatus, }, }; } } } return bestScore >= 55 ? best : undefined; }