lyrics-transcriber 0.34.0__py3-none-any.whl → 0.34.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. lyrics_transcriber/correction/handlers/syllables_match.py +22 -2
  2. lyrics_transcriber/frontend/.gitignore +23 -0
  3. lyrics_transcriber/frontend/README.md +50 -0
  4. lyrics_transcriber/frontend/dist/assets/index-DqFgiUni.js +245 -0
  5. lyrics_transcriber/frontend/dist/index.html +13 -0
  6. lyrics_transcriber/frontend/dist/vite.svg +1 -0
  7. lyrics_transcriber/frontend/eslint.config.js +28 -0
  8. lyrics_transcriber/frontend/index.html +13 -0
  9. lyrics_transcriber/frontend/package-lock.json +4260 -0
  10. lyrics_transcriber/frontend/package.json +37 -0
  11. lyrics_transcriber/frontend/public/vite.svg +1 -0
  12. lyrics_transcriber/frontend/src/App.tsx +192 -0
  13. lyrics_transcriber/frontend/src/api.ts +59 -0
  14. lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +155 -0
  15. lyrics_transcriber/frontend/src/components/DebugPanel.tsx +311 -0
  16. lyrics_transcriber/frontend/src/components/DetailsModal.tsx +297 -0
  17. lyrics_transcriber/frontend/src/components/FileUpload.tsx +77 -0
  18. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +450 -0
  19. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +287 -0
  20. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +157 -0
  21. lyrics_transcriber/frontend/src/components/constants.ts +19 -0
  22. lyrics_transcriber/frontend/src/components/styles.ts +13 -0
  23. lyrics_transcriber/frontend/src/main.tsx +6 -0
  24. lyrics_transcriber/frontend/src/types.ts +158 -0
  25. lyrics_transcriber/frontend/src/vite-env.d.ts +1 -0
  26. lyrics_transcriber/frontend/tsconfig.app.json +26 -0
  27. lyrics_transcriber/frontend/tsconfig.json +25 -0
  28. lyrics_transcriber/frontend/tsconfig.node.json +23 -0
  29. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -0
  30. lyrics_transcriber/frontend/vite.config.d.ts +2 -0
  31. lyrics_transcriber/frontend/vite.config.js +6 -0
  32. lyrics_transcriber/frontend/vite.config.ts +7 -0
  33. lyrics_transcriber/review/server.py +18 -29
  34. {lyrics_transcriber-0.34.0.dist-info → lyrics_transcriber-0.34.2.dist-info}/METADATA +1 -1
  35. {lyrics_transcriber-0.34.0.dist-info → lyrics_transcriber-0.34.2.dist-info}/RECORD +38 -7
  36. {lyrics_transcriber-0.34.0.dist-info → lyrics_transcriber-0.34.2.dist-info}/LICENSE +0 -0
  37. {lyrics_transcriber-0.34.0.dist-info → lyrics_transcriber-0.34.2.dist-info}/WHEEL +0 -0
  38. {lyrics_transcriber-0.34.0.dist-info → lyrics_transcriber-0.34.2.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,287 @@
1
+ import { useEffect, useMemo, useRef } from 'react'
2
+ import { Paper, Typography, Box, Button } from '@mui/material'
3
+ import { AnchorMatchInfo, LyricsData, LyricsSegment } from '../types'
4
+ import { FlashType, ModalContent } from './LyricsAnalyzer'
5
+ import { COLORS } from './constants'
6
+ import { HighlightedWord } from './styles'
7
+
8
+ interface WordClickInfo {
9
+ wordIndex: number
10
+ type: 'anchor' | 'gap' | 'other'
11
+ anchor?: LyricsData['anchor_sequences'][0]
12
+ gap?: LyricsData['gap_sequences'][0]
13
+ }
14
+
15
+ interface ReferenceViewProps {
16
+ referenceTexts: Record<string, string>
17
+ anchors: LyricsData['anchor_sequences']
18
+ gaps: LyricsData['gap_sequences']
19
+ onElementClick: (content: ModalContent) => void
20
+ onWordClick?: (info: WordClickInfo) => void
21
+ flashingType: FlashType
22
+ corrected_segments: LyricsSegment[]
23
+ highlightedWordIndex?: number
24
+ currentSource: 'genius' | 'spotify'
25
+ onSourceChange: (source: 'genius' | 'spotify') => void
26
+ onDebugInfoUpdate?: (info: AnchorMatchInfo[]) => void
27
+ }
28
+
29
+ const normalizeWord = (word: string): string =>
30
+ word.toLowerCase().replace(/[.,!?']/g, '')
31
+
32
+ export default function ReferenceView({
33
+ referenceTexts,
34
+ anchors,
35
+ gaps,
36
+ onElementClick,
37
+ onWordClick,
38
+ flashingType,
39
+ corrected_segments,
40
+ highlightedWordIndex,
41
+ currentSource,
42
+ onSourceChange,
43
+ onDebugInfoUpdate
44
+ }: ReferenceViewProps) {
45
+ // Create a ref to store debug info to avoid dependency cycles
46
+ const debugInfoRef = useRef<AnchorMatchInfo[]>([])
47
+
48
+ const { newlineIndices } = useMemo(() => {
49
+ debugInfoRef.current = corrected_segments.map(segment => ({
50
+ segment: segment.text.trim(),
51
+ lastWord: '',
52
+ normalizedLastWord: '',
53
+ overlappingAnchors: [],
54
+ matchingGap: null,
55
+ debugLog: []
56
+ }));
57
+
58
+ const newlineIndices = new Set(
59
+ corrected_segments.slice(0, -1).map((segment, segmentIndex) => {
60
+ const segmentText = segment.text.trim()
61
+ const segmentWords = segmentText.split(/\s+/)
62
+ const lastWord = segmentWords[segmentWords.length - 1]
63
+ const normalizedLastWord = normalizeWord(lastWord)
64
+
65
+ debugInfoRef.current[segmentIndex].debugLog?.push(
66
+ `Processing segment: "${segmentText}"\n` +
67
+ ` Words: ${segmentWords.join('|')}\n` +
68
+ ` Last word: "${lastWord}"\n` +
69
+ ` Normalized last word: "${normalizedLastWord}"`
70
+ )
71
+
72
+ // Calculate word position
73
+ const segmentStartWord = corrected_segments
74
+ .slice(0, segmentIndex)
75
+ .reduce((acc, s) => acc + s.text.trim().split(/\s+/).length, 0)
76
+ const lastWordPosition = segmentStartWord + segmentWords.length - 1
77
+
78
+ // Try to find the anchor containing this word
79
+ const matchingAnchor = anchors.find(a => {
80
+ const start = a.transcription_position
81
+ const end = start + a.length - 1
82
+ const isMatch = lastWordPosition >= start && lastWordPosition <= end
83
+
84
+ debugInfoRef.current[segmentIndex].debugLog?.push(
85
+ `Checking anchor: "${a.text}"\n` +
86
+ ` Position range: ${start}-${end}\n` +
87
+ ` Last word position: ${lastWordPosition}\n` +
88
+ ` Is in range: ${isMatch}\n` +
89
+ ` Words: ${a.words.join('|')}`
90
+ )
91
+
92
+ return isMatch
93
+ })
94
+
95
+ if (matchingAnchor?.reference_positions[currentSource] !== undefined) {
96
+ const anchorWords = matchingAnchor.words
97
+ const wordIndex = anchorWords.findIndex(w => {
98
+ const normalizedAnchorWord = normalizeWord(w)
99
+ const matches = normalizedAnchorWord === normalizedLastWord
100
+
101
+ debugInfoRef.current[segmentIndex].debugLog?.push(
102
+ `Comparing words:\n` +
103
+ ` Anchor word: "${w}" (normalized: "${normalizedAnchorWord}")\n` +
104
+ ` Segment word: "${lastWord}" (normalized: "${normalizedLastWord}")\n` +
105
+ ` Matches: ${matches}`
106
+ )
107
+
108
+ return matches
109
+ })
110
+
111
+ if (wordIndex !== -1) {
112
+ const position = matchingAnchor.reference_positions[currentSource] + wordIndex
113
+
114
+ debugInfoRef.current[segmentIndex].debugLog?.push(
115
+ `Found match:\n` +
116
+ ` Word index in anchor: ${wordIndex}\n` +
117
+ ` Reference position: ${matchingAnchor.reference_positions[currentSource]}\n` +
118
+ ` Final position: ${position}`
119
+ )
120
+
121
+ // Update debug info with word matching details
122
+ debugInfoRef.current[segmentIndex] = {
123
+ ...debugInfoRef.current[segmentIndex],
124
+ lastWord,
125
+ normalizedLastWord,
126
+ overlappingAnchors: [{
127
+ text: matchingAnchor.text,
128
+ range: [matchingAnchor.transcription_position, matchingAnchor.transcription_position + matchingAnchor.length - 1],
129
+ words: anchorWords,
130
+ hasMatchingWord: true
131
+ }],
132
+ wordPositionDebug: {
133
+ anchorWords,
134
+ wordIndex,
135
+ referencePosition: matchingAnchor.reference_positions[currentSource],
136
+ finalPosition: position,
137
+ normalizedWords: {
138
+ anchor: normalizeWord(anchorWords[wordIndex]),
139
+ segment: normalizedLastWord
140
+ }
141
+ }
142
+ }
143
+
144
+ return position
145
+ }
146
+ }
147
+
148
+ return null
149
+ }).filter((pos): pos is number => pos !== null && pos >= 0)
150
+ )
151
+ return { newlineIndices }
152
+ }, [corrected_segments, anchors, currentSource])
153
+
154
+ // Update debug info whenever it changes
155
+ useEffect(() => {
156
+ onDebugInfoUpdate?.(debugInfoRef.current)
157
+ }, [onDebugInfoUpdate])
158
+
159
+ const renderHighlightedText = () => {
160
+ const elements: React.ReactNode[] = []
161
+ const words = referenceTexts[currentSource].split(/\s+/)
162
+ let currentIndex = 0
163
+
164
+ words.forEach((word, index) => {
165
+ // Add the word element
166
+ const thisWordIndex = currentIndex
167
+ const anchor = anchors.find(a => {
168
+ const position = a.reference_positions[currentSource]
169
+ if (position === undefined) return false
170
+ return thisWordIndex >= position && thisWordIndex < position + a.length
171
+ })
172
+ const correctedGap = gaps.find(g => {
173
+ if (!g.corrections.length) return false
174
+ const correction = g.corrections[0]
175
+ const position = correction.reference_positions?.[currentSource]
176
+ if (position === undefined) return false
177
+ return thisWordIndex >= position && thisWordIndex < position + correction.length
178
+ })
179
+
180
+ elements.push(
181
+ <HighlightedWord
182
+ key={`${word}-${index}`}
183
+ shouldFlash={flashingType === 'word' && highlightedWordIndex === thisWordIndex}
184
+ style={{
185
+ backgroundColor: flashingType === 'word' && highlightedWordIndex === thisWordIndex
186
+ ? COLORS.highlighted
187
+ : anchor
188
+ ? COLORS.anchor
189
+ : correctedGap
190
+ ? COLORS.corrected
191
+ : 'transparent',
192
+ padding: (anchor || correctedGap) ? '2px 4px' : '0',
193
+ borderRadius: '3px',
194
+ cursor: 'pointer',
195
+ }}
196
+ onClick={(e) => {
197
+ if (e.detail === 1) {
198
+ setTimeout(() => {
199
+ if (!e.defaultPrevented) {
200
+ onWordClick?.({
201
+ wordIndex: thisWordIndex,
202
+ type: anchor ? 'anchor' : correctedGap ? 'gap' : 'other',
203
+ anchor,
204
+ gap: correctedGap
205
+ })
206
+ }
207
+ }, 200)
208
+ }
209
+ }}
210
+ onDoubleClick={(e) => {
211
+ e.preventDefault() // Prevent single-click from firing
212
+ if (anchor) {
213
+ onElementClick({
214
+ type: 'anchor',
215
+ data: {
216
+ ...anchor,
217
+ position: thisWordIndex
218
+ }
219
+ })
220
+ } else if (correctedGap) {
221
+ onElementClick({
222
+ type: 'gap',
223
+ data: {
224
+ ...correctedGap,
225
+ position: thisWordIndex,
226
+ word: word
227
+ }
228
+ })
229
+ }
230
+ }}
231
+ >
232
+ {word}
233
+ </HighlightedWord>
234
+ )
235
+
236
+ // Check if we need to add a newline after this word
237
+ if (newlineIndices.has(thisWordIndex)) {
238
+ elements.push(<br key={`br-${index}`} />)
239
+ } else {
240
+ // Only add space if not adding newline
241
+ elements.push(' ')
242
+ }
243
+
244
+ currentIndex++
245
+ })
246
+
247
+ return elements
248
+ }
249
+
250
+ return (
251
+ <Paper sx={{ p: 2 }}>
252
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
253
+ <Typography variant="h6">
254
+ Reference Text
255
+ </Typography>
256
+ <Box>
257
+ <Button
258
+ size="small"
259
+ variant={currentSource === 'genius' ? 'contained' : 'outlined'}
260
+ onClick={() => onSourceChange('genius')}
261
+ sx={{ mr: 1 }}
262
+ >
263
+ Genius
264
+ </Button>
265
+ <Button
266
+ size="small"
267
+ variant={currentSource === 'spotify' ? 'contained' : 'outlined'}
268
+ onClick={() => onSourceChange('spotify')}
269
+ >
270
+ Spotify
271
+ </Button>
272
+ </Box>
273
+ </Box>
274
+ <Typography
275
+ component="pre"
276
+ sx={{
277
+ fontFamily: 'monospace',
278
+ whiteSpace: 'pre-wrap',
279
+ margin: 0,
280
+ lineHeight: 1.5,
281
+ }}
282
+ >
283
+ {renderHighlightedText()}
284
+ </Typography>
285
+ </Paper>
286
+ )
287
+ }
@@ -0,0 +1,157 @@
1
+ import { Paper, Typography } from '@mui/material'
2
+ import { CorrectionData, AnchorSequence, HighlightInfo } from '../types'
3
+ import { FlashType, ModalContent } from './LyricsAnalyzer'
4
+ import { COLORS } from './constants'
5
+ import { HighlightedWord } from './styles'
6
+
7
+ interface WordClickInfo {
8
+ wordIndex: number
9
+ type: 'anchor' | 'gap' | 'other'
10
+ anchor?: AnchorSequence
11
+ gap?: CorrectionData['gap_sequences'][0]
12
+ }
13
+
14
+ interface TranscriptionViewProps {
15
+ data: CorrectionData
16
+ onElementClick: (content: ModalContent) => void
17
+ onWordClick?: (info: WordClickInfo) => void
18
+ flashingType: FlashType
19
+ highlightInfo: HighlightInfo | null
20
+ }
21
+
22
+ export default function TranscriptionView({
23
+ data,
24
+ onElementClick,
25
+ onWordClick,
26
+ flashingType,
27
+ highlightInfo
28
+ }: TranscriptionViewProps) {
29
+ const renderHighlightedText = () => {
30
+ const normalizedText = data.corrected_text.replace(/\n\n+/g, '\n')
31
+ const words = normalizedText.split(/(\s+)/)
32
+ let wordIndex = 0 // Track actual word positions, ignoring whitespace
33
+
34
+ // Build a map of original positions to their corrections
35
+ const correctionMap = new Map()
36
+ data.gap_sequences.forEach(gap => {
37
+ gap.corrections.forEach(c => {
38
+ correctionMap.set(c.original_position, {
39
+ original: c.original_word,
40
+ corrected: c.corrected_word,
41
+ is_deletion: c.is_deletion,
42
+ split_total: c.split_total
43
+ })
44
+ })
45
+ })
46
+
47
+ return words.map((word, index) => {
48
+ if (/^\s+$/.test(word)) {
49
+ return word
50
+ }
51
+
52
+ const currentWordIndex = wordIndex
53
+ const anchor = data.anchor_sequences.find(anchor => {
54
+ const start = anchor.transcription_position
55
+ const end = start + anchor.length
56
+ return currentWordIndex >= start && currentWordIndex < end
57
+ })
58
+
59
+ const gap = data.gap_sequences.find(g => {
60
+ const start = g.transcription_position
61
+ const end = start + g.length
62
+ return currentWordIndex >= start && currentWordIndex < end
63
+ })
64
+
65
+ const hasCorrections = gap ? gap.corrections.length > 0 : false
66
+
67
+ const shouldFlash = Boolean(
68
+ (flashingType === 'anchor' && anchor) ||
69
+ (flashingType === 'corrected' && hasCorrections) ||
70
+ (flashingType === 'uncorrected' && gap && !hasCorrections) ||
71
+ (flashingType === 'word' && highlightInfo?.type === 'anchor' && anchor && (
72
+ anchor.transcription_position === highlightInfo.transcriptionIndex &&
73
+ currentWordIndex >= anchor.transcription_position &&
74
+ currentWordIndex < anchor.transcription_position + anchor.length
75
+ ))
76
+ )
77
+
78
+ const wordElement = (
79
+ <HighlightedWord
80
+ key={`${word}-${index}-${shouldFlash}`}
81
+ shouldFlash={shouldFlash}
82
+ style={{
83
+ backgroundColor: anchor
84
+ ? COLORS.anchor
85
+ : hasCorrections
86
+ ? COLORS.corrected
87
+ : gap
88
+ ? COLORS.uncorrectedGap
89
+ : 'transparent',
90
+ padding: anchor || gap ? '2px 4px' : '0',
91
+ borderRadius: '3px',
92
+ cursor: 'pointer',
93
+ }}
94
+ onClick={(e) => {
95
+ if (e.detail === 1) {
96
+ setTimeout(() => {
97
+ if (!e.defaultPrevented) {
98
+ onWordClick?.({
99
+ wordIndex: currentWordIndex,
100
+ type: anchor ? 'anchor' : gap ? 'gap' : 'other',
101
+ anchor,
102
+ gap
103
+ })
104
+ }
105
+ }, 200)
106
+ }
107
+ }}
108
+ onDoubleClick={(e) => {
109
+ e.preventDefault() // Prevent single-click from firing
110
+ if (anchor) {
111
+ onElementClick({
112
+ type: 'anchor',
113
+ data: {
114
+ ...anchor,
115
+ position: currentWordIndex
116
+ }
117
+ })
118
+ } else if (gap) {
119
+ onElementClick({
120
+ type: 'gap',
121
+ data: {
122
+ ...gap,
123
+ position: currentWordIndex,
124
+ word: word
125
+ }
126
+ })
127
+ }
128
+ }}
129
+ >
130
+ {word}
131
+ </HighlightedWord>
132
+ )
133
+
134
+ wordIndex++
135
+ return wordElement
136
+ })
137
+ }
138
+
139
+ return (
140
+ <Paper sx={{ p: 2 }}>
141
+ <Typography variant="h6" gutterBottom>
142
+ Corrected Transcription
143
+ </Typography>
144
+ <Typography
145
+ component="pre"
146
+ sx={{
147
+ fontFamily: 'monospace',
148
+ whiteSpace: 'pre-wrap',
149
+ margin: 0,
150
+ lineHeight: 1.5,
151
+ }}
152
+ >
153
+ {renderHighlightedText()}
154
+ </Typography>
155
+ </Paper>
156
+ )
157
+ }
@@ -0,0 +1,19 @@
1
+ import { keyframes } from '@mui/system'
2
+
3
+ export const COLORS = {
4
+ anchor: '#e3f2fd', // Pale blue
5
+ corrected: '#e8f5e9', // Pale green
6
+ uncorrectedGap: '#fff3e0', // Pale orange
7
+ highlighted: '#ffeb3b', // or any color you prefer for highlighting
8
+ } as const
9
+
10
+ export const flashAnimation = keyframes`
11
+ 0%, 100% {
12
+ opacity: 1;
13
+ background-color: inherit;
14
+ }
15
+ 50% {
16
+ opacity: 0.6;
17
+ background-color: ${COLORS.highlighted};
18
+ }
19
+ `
@@ -0,0 +1,13 @@
1
+ import { styled } from '@mui/system'
2
+ import { flashAnimation } from './constants'
3
+
4
+ export const HighlightedWord = styled('span')<{ shouldFlash: boolean }>(
5
+ ({ shouldFlash }) => ({
6
+ display: 'inline-block',
7
+ marginRight: '0.25em',
8
+ transition: 'background-color 0.2s ease',
9
+ ...(shouldFlash && {
10
+ animation: `${flashAnimation} 0.4s ease-in-out 3`,
11
+ }),
12
+ })
13
+ )
@@ -0,0 +1,6 @@
1
+ import ReactDOM from 'react-dom/client'
2
+ import App from './App'
3
+
4
+ ReactDOM.createRoot(document.getElementById('root')!).render(
5
+ <App />
6
+ )
@@ -0,0 +1,158 @@
1
+ export interface Word {
2
+ text: string
3
+ start_time: number
4
+ end_time: number
5
+ confidence?: number
6
+ }
7
+
8
+ export interface LyricsSegment {
9
+ text: string
10
+ words: Word[]
11
+ start_time: number
12
+ end_time: number
13
+ }
14
+
15
+ export interface WordCorrection {
16
+ original_word: string
17
+ corrected_word: string
18
+ segment_index: number
19
+ original_position: number
20
+ source: string
21
+ confidence: number
22
+ reason: string
23
+ alternatives: Record<string, number>
24
+ is_deletion: boolean
25
+ split_index?: number
26
+ split_total?: number
27
+ reference_positions?: Record<string, number>
28
+ length: number
29
+ }
30
+
31
+ export interface PhraseScore {
32
+ phrase_type: string
33
+ natural_break_score: number
34
+ length_score: number
35
+ total_score: number
36
+ }
37
+
38
+ export interface AnchorSequence {
39
+ words: string[]
40
+ text: string
41
+ length: number
42
+ transcription_position: number
43
+ reference_positions: Record<string, number>
44
+ confidence: number
45
+ phrase_score: PhraseScore
46
+ total_score: number
47
+ }
48
+
49
+ export interface GapSequence {
50
+ words: string[]
51
+ text: string
52
+ length: number
53
+ transcription_position: number
54
+ preceding_anchor: AnchorSequence | null
55
+ following_anchor: AnchorSequence | null
56
+ reference_words: {
57
+ spotify?: string[]
58
+ genius?: string[]
59
+ }
60
+ reference_words_original?: {
61
+ spotify?: string[]
62
+ genius?: string[]
63
+ }
64
+ corrections: WordCorrection[]
65
+ }
66
+
67
+ export interface LyricsData {
68
+ transcribed_text: string
69
+ corrected_text: string
70
+ original_segments: LyricsSegment[]
71
+ metadata: {
72
+ anchor_sequences_count: number
73
+ gap_sequences_count: number
74
+ total_words: number
75
+ correction_ratio: number
76
+ }
77
+ anchor_sequences: AnchorSequence[]
78
+ gap_sequences: GapSequence[]
79
+ corrected_segments: LyricsSegment[]
80
+ corrections_made: number
81
+ confidence: number
82
+ corrections: WordCorrection[]
83
+ reference_texts: Record<string, string>
84
+ }
85
+
86
+ export interface CorrectionData {
87
+ transcribed_text: string
88
+ original_segments: LyricsSegment[]
89
+ reference_texts: Record<string, string>
90
+ anchor_sequences: AnchorSequence[]
91
+ gap_sequences: GapSequence[]
92
+ resized_segments: LyricsSegment[]
93
+ corrected_text: string
94
+ corrections_made: number
95
+ confidence: number
96
+ corrections: WordCorrection[]
97
+ corrected_segments: LyricsSegment[]
98
+ metadata: {
99
+ anchor_sequences_count: number
100
+ gap_sequences_count: number
101
+ total_words: number
102
+ correction_ratio: number
103
+ }
104
+ }
105
+
106
+ export interface HighlightInfo {
107
+ transcriptionIndex?: number
108
+ transcriptionLength?: number
109
+ referenceIndices: {
110
+ genius?: number
111
+ spotify?: number
112
+ }
113
+ referenceLength?: number
114
+ type: 'single' | 'gap' | 'anchor'
115
+ }
116
+
117
+ export interface AnchorMatchInfo {
118
+ segment: string
119
+ lastWord: string
120
+ normalizedLastWord: string
121
+ overlappingAnchors: Array<{
122
+ text: string
123
+ range: [number, number]
124
+ words: string[]
125
+ hasMatchingWord: boolean
126
+ }>
127
+ matchingGap: {
128
+ text: string
129
+ position: number
130
+ length: number
131
+ corrections: Array<{
132
+ word: string
133
+ referencePosition: number | undefined
134
+ }>
135
+ followingAnchor: {
136
+ text: string
137
+ position: number | undefined
138
+ } | null
139
+ } | null
140
+ highlightDebug?: Array<{
141
+ wordIndex: number
142
+ refPos: number | undefined
143
+ highlightPos: number | undefined
144
+ anchorLength: number
145
+ isInRange: boolean
146
+ }>
147
+ wordPositionDebug?: {
148
+ anchorWords: string[]
149
+ wordIndex: number
150
+ referencePosition: number
151
+ finalPosition: number
152
+ normalizedWords: {
153
+ anchor: string
154
+ segment: string
155
+ }
156
+ }
157
+ debugLog?: string[]
158
+ }
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2020",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "isolatedModules": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+ "jsx": "react-jsx",
17
+
18
+ /* Linting */
19
+ "strict": true,
20
+ "noUnusedLocals": true,
21
+ "noUnusedParameters": true,
22
+ "noFallthroughCasesInSwitch": true,
23
+ "noUncheckedSideEffectImports": true
24
+ },
25
+ "include": ["src"]
26
+ }