lyrics-transcriber 0.41.0__py3-none-any.whl → 0.42.0__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 (77) hide show
  1. lyrics_transcriber/core/controller.py +30 -52
  2. lyrics_transcriber/correction/anchor_sequence.py +325 -150
  3. lyrics_transcriber/correction/corrector.py +224 -107
  4. lyrics_transcriber/correction/handlers/base.py +28 -10
  5. lyrics_transcriber/correction/handlers/extend_anchor.py +47 -24
  6. lyrics_transcriber/correction/handlers/levenshtein.py +75 -33
  7. lyrics_transcriber/correction/handlers/llm.py +290 -0
  8. lyrics_transcriber/correction/handlers/no_space_punct_match.py +81 -36
  9. lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +46 -26
  10. lyrics_transcriber/correction/handlers/repeat.py +28 -11
  11. lyrics_transcriber/correction/handlers/sound_alike.py +68 -32
  12. lyrics_transcriber/correction/handlers/syllables_match.py +80 -30
  13. lyrics_transcriber/correction/handlers/word_count_match.py +36 -19
  14. lyrics_transcriber/correction/handlers/word_operations.py +68 -22
  15. lyrics_transcriber/correction/text_utils.py +3 -7
  16. lyrics_transcriber/frontend/.yarn/install-state.gz +0 -0
  17. lyrics_transcriber/frontend/.yarn/releases/yarn-4.6.0.cjs +934 -0
  18. lyrics_transcriber/frontend/.yarnrc.yml +3 -0
  19. lyrics_transcriber/frontend/dist/assets/{index-DKnNJHRK.js → index-coH8y7gV.js} +16284 -9032
  20. lyrics_transcriber/frontend/dist/assets/index-coH8y7gV.js.map +1 -0
  21. lyrics_transcriber/frontend/dist/index.html +1 -1
  22. lyrics_transcriber/frontend/package.json +6 -2
  23. lyrics_transcriber/frontend/src/App.tsx +18 -2
  24. lyrics_transcriber/frontend/src/api.ts +103 -6
  25. lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +7 -6
  26. lyrics_transcriber/frontend/src/components/DetailsModal.tsx +86 -59
  27. lyrics_transcriber/frontend/src/components/EditModal.tsx +93 -43
  28. lyrics_transcriber/frontend/src/components/FileUpload.tsx +2 -2
  29. lyrics_transcriber/frontend/src/components/Header.tsx +251 -0
  30. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +303 -265
  31. lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +117 -0
  32. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +125 -40
  33. lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +129 -115
  34. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +59 -78
  35. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +40 -16
  36. lyrics_transcriber/frontend/src/components/WordEditControls.tsx +4 -10
  37. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +137 -68
  38. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +1 -1
  39. lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +85 -115
  40. lyrics_transcriber/frontend/src/components/shared/types.js +2 -0
  41. lyrics_transcriber/frontend/src/components/shared/types.ts +15 -7
  42. lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +35 -0
  43. lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +78 -0
  44. lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +7 -7
  45. lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +121 -0
  46. lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +22 -0
  47. lyrics_transcriber/frontend/src/types.js +2 -0
  48. lyrics_transcriber/frontend/src/types.ts +70 -49
  49. lyrics_transcriber/frontend/src/validation.ts +132 -0
  50. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
  51. lyrics_transcriber/frontend/yarn.lock +3752 -0
  52. lyrics_transcriber/lyrics/base_lyrics_provider.py +75 -12
  53. lyrics_transcriber/lyrics/file_provider.py +6 -5
  54. lyrics_transcriber/lyrics/genius.py +5 -2
  55. lyrics_transcriber/lyrics/spotify.py +58 -21
  56. lyrics_transcriber/output/ass/config.py +16 -5
  57. lyrics_transcriber/output/cdg.py +1 -1
  58. lyrics_transcriber/output/generator.py +22 -8
  59. lyrics_transcriber/output/plain_text.py +15 -10
  60. lyrics_transcriber/output/segment_resizer.py +16 -3
  61. lyrics_transcriber/output/subtitles.py +27 -1
  62. lyrics_transcriber/output/video.py +107 -1
  63. lyrics_transcriber/review/__init__.py +0 -1
  64. lyrics_transcriber/review/server.py +337 -164
  65. lyrics_transcriber/transcribers/audioshake.py +3 -0
  66. lyrics_transcriber/transcribers/base_transcriber.py +11 -3
  67. lyrics_transcriber/transcribers/whisper.py +11 -1
  68. lyrics_transcriber/types.py +151 -105
  69. lyrics_transcriber/utils/word_utils.py +27 -0
  70. {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.42.0.dist-info}/METADATA +3 -1
  71. {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.42.0.dist-info}/RECORD +74 -61
  72. {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.42.0.dist-info}/WHEEL +1 -1
  73. lyrics_transcriber/frontend/dist/assets/index-DKnNJHRK.js.map +0 -1
  74. lyrics_transcriber/frontend/package-lock.json +0 -4260
  75. lyrics_transcriber/frontend/src/components/shared/utils/initializeDataWithIds.tsx +0 -202
  76. {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.42.0.dist-info}/LICENSE +0 -0
  77. {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.42.0.dist-info}/entry_points.txt +0 -0
@@ -5,7 +5,7 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>Lyrics Transcriber Analyzer</title>
8
- <script type="module" crossorigin src="/assets/index-DKnNJHRK.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-coH8y7gV.js"></script>
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
@@ -18,8 +18,11 @@
18
18
  "@emotion/styled": "^11.14.0",
19
19
  "@mui/icons-material": "^6.3.0",
20
20
  "@mui/material": "^6.3.0",
21
+ "@mui/system": "^6.4.3",
22
+ "nanoid": "^5.0.9",
21
23
  "react": "^18.3.1",
22
- "react-dom": "^18.3.1"
24
+ "react-dom": "^18.3.1",
25
+ "zod": "^3.24.1"
23
26
  },
24
27
  "devDependencies": {
25
28
  "@eslint/js": "^9.17.0",
@@ -34,5 +37,6 @@
34
37
  "typescript": "~5.6.2",
35
38
  "typescript-eslint": "^8.18.2",
36
39
  "vite": "^6.0.5"
37
- }
40
+ },
41
+ "packageManager": "yarn@4.6.0"
38
42
  }
@@ -12,16 +12,21 @@ export default function App() {
12
12
  const [error, setError] = useState<string | null>(null)
13
13
  const [apiClient, setApiClient] = useState<ApiClient | null>(null)
14
14
  const [isReadOnly, setIsReadOnly] = useState(true)
15
+ const [audioHash, setAudioHash] = useState<string>('')
15
16
 
16
17
  useEffect(() => {
17
18
  // Parse query parameters
18
19
  const params = new URLSearchParams(window.location.search)
19
20
  const encodedApiUrl = params.get('baseApiUrl')
21
+ const audioHashParam = params.get('audioHash')
20
22
 
21
23
  if (encodedApiUrl) {
22
24
  const baseApiUrl = decodeURIComponent(encodedApiUrl)
23
25
  setApiClient(new LiveApiClient(baseApiUrl))
24
26
  setIsReadOnly(false)
27
+ if (audioHashParam) {
28
+ setAudioHash(audioHashParam)
29
+ }
25
30
  // Fetch initial data
26
31
  fetchData(baseApiUrl)
27
32
  } else {
@@ -34,6 +39,7 @@ export default function App() {
34
39
  try {
35
40
  const client = new LiveApiClient(baseUrl)
36
41
  const data = await client.getCorrectionData()
42
+ console.log('Full correction data from API:', data)
37
43
  setData(data)
38
44
  } catch (err) {
39
45
  const error = err as Error
@@ -52,9 +58,18 @@ export default function App() {
52
58
 
53
59
  try {
54
60
  const text = await file.text()
55
- console.log('File contents:', text.slice(0, 500) + '...') // Show first 500 chars
56
61
  const parsedData = JSON.parse(text) as CorrectionData
57
- console.log('Parsed file data:', parsedData)
62
+ console.log('File data loaded:', {
63
+ sampleGap: parsedData.gap_sequences?.[0],
64
+ sampleWord: parsedData.corrected_segments?.[0]?.words?.[0],
65
+ sampleCorrection: parsedData.corrections?.[0]
66
+ })
67
+
68
+ // Validate the structure
69
+ if (!parsedData.corrected_segments || !parsedData.gap_sequences) {
70
+ throw new Error('Invalid file format: missing required fields')
71
+ }
72
+
58
73
  setData(parsedData)
59
74
  } catch (err) {
60
75
  const error = err as Error
@@ -189,6 +204,7 @@ export default function App() {
189
204
  onShowMetadata={() => setShowMetadata(true)}
190
205
  apiClient={apiClient}
191
206
  isReadOnly={isReadOnly}
207
+ audioHash={audioHash}
192
208
  />
193
209
  {renderMetadataModal()}
194
210
  </Box>
@@ -1,10 +1,15 @@
1
1
  import { CorrectionData } from './types';
2
+ import { validateCorrectionData } from './validation';
2
3
 
3
4
  // New file to handle API communication
4
5
  export interface ApiClient {
5
6
  getCorrectionData: () => Promise<CorrectionData>;
6
7
  submitCorrections: (data: CorrectionData) => Promise<void>;
7
- getAudioUrl: () => string;
8
+ getAudioUrl: (audioHash: string) => string;
9
+ generatePreviewVideo: (data: CorrectionData) => Promise<PreviewVideoResponse>;
10
+ getPreviewVideoUrl: (previewHash: string) => string;
11
+ updateHandlers: (enabledHandlers: string[]) => Promise<CorrectionData>;
12
+ isUpdatingHandlers?: boolean;
8
13
  }
9
14
 
10
15
  // Add new interface for the minimal update payload
@@ -13,18 +18,34 @@ interface CorrectionUpdate {
13
18
  corrected_segments: CorrectionData['corrected_segments'];
14
19
  }
15
20
 
21
+ // Add new interface for preview response
22
+ interface PreviewVideoResponse {
23
+ status: "success" | "error";
24
+ preview_hash?: string;
25
+ message?: string;
26
+ }
27
+
16
28
  export class LiveApiClient implements ApiClient {
17
29
  constructor(private baseUrl: string) {
18
30
  this.baseUrl = baseUrl.replace(/\/$/, '')
19
31
  }
20
32
 
33
+ public isUpdatingHandlers = false;
34
+
21
35
  async getCorrectionData(): Promise<CorrectionData> {
22
36
  const response = await fetch(`${this.baseUrl}/correction-data`);
23
37
  if (!response.ok) {
24
38
  throw new Error(`API error: ${response.statusText}`);
25
39
  }
26
- const data = await response.json();
27
- return data;
40
+ const rawData = await response.json();
41
+
42
+ try {
43
+ // This will throw if validation fails
44
+ return validateCorrectionData(rawData);
45
+ } catch (error) {
46
+ console.error('Data validation failed:', error);
47
+ throw new Error('Invalid data received from server: missing or incorrect fields');
48
+ }
28
49
  }
29
50
 
30
51
  async submitCorrections(data: CorrectionData): Promise<void> {
@@ -47,8 +68,68 @@ export class LiveApiClient implements ApiClient {
47
68
  }
48
69
  }
49
70
 
50
- getAudioUrl(): string {
51
- return `${this.baseUrl}/audio`
71
+ getAudioUrl(audioHash: string): string {
72
+ return `${this.baseUrl}/audio/${audioHash}`
73
+ }
74
+
75
+ async generatePreviewVideo(data: CorrectionData): Promise<PreviewVideoResponse> {
76
+ // Extract only the needed fields, just like in submitCorrections
77
+ const updatePayload: CorrectionUpdate = {
78
+ corrections: data.corrections,
79
+ corrected_segments: data.corrected_segments
80
+ };
81
+
82
+ const response = await fetch(`${this.baseUrl}/preview-video`, {
83
+ method: 'POST',
84
+ headers: {
85
+ 'Content-Type': 'application/json',
86
+ },
87
+ body: JSON.stringify(updatePayload)
88
+ });
89
+
90
+ if (!response.ok) {
91
+ return {
92
+ status: 'error',
93
+ message: `API error: ${response.statusText}`
94
+ };
95
+ }
96
+
97
+ return await response.json();
98
+ }
99
+
100
+ getPreviewVideoUrl(previewHash: string): string {
101
+ return `${this.baseUrl}/preview-video/${previewHash}`;
102
+ }
103
+
104
+ async updateHandlers(enabledHandlers: string[]): Promise<CorrectionData> {
105
+ console.log('API: Starting handler update...');
106
+ this.isUpdatingHandlers = true;
107
+ console.log('API: Set isUpdatingHandlers to', this.isUpdatingHandlers);
108
+
109
+ try {
110
+ const response = await fetch(`${this.baseUrl}/handlers`, {
111
+ method: 'POST',
112
+ headers: {
113
+ 'Content-Type': 'application/json',
114
+ },
115
+ body: JSON.stringify(enabledHandlers)
116
+ });
117
+
118
+ if (!response.ok) {
119
+ throw new Error(`API error: ${response.statusText}`);
120
+ }
121
+
122
+ const data = await response.json();
123
+ if (data.status === 'error') {
124
+ throw new Error(data.message || 'Failed to update handlers');
125
+ }
126
+
127
+ console.log('API: Handler update successful');
128
+ return validateCorrectionData(data.data);
129
+ } finally {
130
+ this.isUpdatingHandlers = false;
131
+ console.log('API: Set isUpdatingHandlers to', this.isUpdatingHandlers);
132
+ }
52
133
  }
53
134
  }
54
135
 
@@ -62,7 +143,23 @@ export class FileOnlyClient implements ApiClient {
62
143
  throw new Error('Not supported in file-only mode');
63
144
  }
64
145
 
65
- getAudioUrl(): string {
146
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
147
+ getAudioUrl(_audioHash: string): string {
148
+ throw new Error('Not supported in file-only mode');
149
+ }
150
+
151
+ async generatePreviewVideo(): Promise<PreviewVideoResponse> {
152
+ throw new Error('Not supported in file-only mode');
153
+ }
154
+
155
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
156
+ getPreviewVideoUrl(_previewHash: string): string {
157
+ throw new Error('Not supported in file-only mode');
158
+ }
159
+
160
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
161
+ async updateHandlers(_enabledHandlers: string[]): Promise<CorrectionData> {
66
162
  throw new Error('Not supported in file-only mode');
67
163
  }
68
164
  }
165
+
@@ -6,10 +6,11 @@ import { ApiClient } from '../api'
6
6
 
7
7
  interface AudioPlayerProps {
8
8
  apiClient: ApiClient | null,
9
- onTimeUpdate?: (time: number) => void
9
+ onTimeUpdate?: (time: number) => void,
10
+ audioHash: string
10
11
  }
11
12
 
12
- export default function AudioPlayer({ apiClient, onTimeUpdate }: AudioPlayerProps) {
13
+ export default function AudioPlayer({ apiClient, onTimeUpdate, audioHash }: AudioPlayerProps) {
13
14
  const [isPlaying, setIsPlaying] = useState(false)
14
15
  const [currentTime, setCurrentTime] = useState(0)
15
16
  const [duration, setDuration] = useState(0)
@@ -18,7 +19,7 @@ export default function AudioPlayer({ apiClient, onTimeUpdate }: AudioPlayerProp
18
19
  useEffect(() => {
19
20
  if (!apiClient) return
20
21
 
21
- const audio = new Audio(apiClient.getAudioUrl())
22
+ const audio = new Audio(apiClient.getAudioUrl(audioHash))
22
23
  audioRef.current = audio
23
24
 
24
25
  // Add requestAnimationFrame for smoother updates
@@ -55,7 +56,7 @@ export default function AudioPlayer({ apiClient, onTimeUpdate }: AudioPlayerProp
55
56
  audio.src = ''
56
57
  audioRef.current = null
57
58
  }
58
- }, [apiClient, onTimeUpdate])
59
+ }, [apiClient, onTimeUpdate, audioHash])
59
60
 
60
61
  const handlePlayPause = () => {
61
62
  if (!audioRef.current) return
@@ -130,7 +131,7 @@ export default function AudioPlayer({ apiClient, onTimeUpdate }: AudioPlayerProp
130
131
  <Typography variant="body2" color="text.secondary" sx={{ mr: 1 }}>
131
132
  Playback:
132
133
  </Typography>
133
-
134
+
134
135
  <IconButton
135
136
  onClick={handlePlayPause}
136
137
  size="small"
@@ -148,7 +149,7 @@ export default function AudioPlayer({ apiClient, onTimeUpdate }: AudioPlayerProp
148
149
  max={duration}
149
150
  onChange={handleSeek}
150
151
  size="small"
151
- sx={{
152
+ sx={{
152
153
  width: 200,
153
154
  mx: 1,
154
155
  '& .MuiSlider-thumb': {
@@ -9,32 +9,78 @@ import {
9
9
  } from '@mui/material'
10
10
  import CloseIcon from '@mui/icons-material/Close'
11
11
  import { ModalContent } from './LyricsAnalyzer'
12
+ import { WordCorrection, ReferenceSource, AnchorSequence } from '../types'
13
+ import { getWordsFromIds } from './shared/utils/wordUtils'
12
14
 
13
15
  interface DetailsModalProps {
14
16
  open: boolean
15
17
  content: ModalContent | null
16
18
  onClose: () => void
19
+ allCorrections: WordCorrection[]
20
+ referenceLyrics?: Record<string, ReferenceSource>
21
+ }
22
+
23
+ function formatReferenceWords(content: ModalContent | null, referenceLyrics?: Record<string, ReferenceSource>): string {
24
+ if (!content || !referenceLyrics) return ''
25
+
26
+ return Object.entries(content.data.reference_word_ids)
27
+ .map(([source, wordIds]) => {
28
+ const words = getWordsFromIds(
29
+ referenceLyrics[source]?.segments ?? [],
30
+ wordIds
31
+ )
32
+ return `${source}: "${words.map(w => w.text).join(' ')}"`
33
+ })
34
+ .join('\n')
35
+ }
36
+
37
+ function getAnchorText(anchorId: string | null, anchors: AnchorSequence[], referenceLyrics?: Record<string, ReferenceSource>): string {
38
+ if (!anchorId || !referenceLyrics) return anchorId || ''
39
+
40
+ const anchor = anchors.find(a => a.id === anchorId)
41
+ if (!anchor) return anchorId
42
+
43
+ // Get the first source's words as representative text
44
+ const firstSource = Object.entries(anchor.reference_word_ids)[0]
45
+ if (!firstSource) return anchorId
46
+
47
+ const [source, wordIds] = firstSource
48
+ const words = getWordsFromIds(
49
+ referenceLyrics[source]?.segments ?? [],
50
+ wordIds
51
+ )
52
+ return `${anchorId} ("${words.map(w => w.text).join(' ')}")`
17
53
  }
18
54
 
19
55
  export default function DetailsModal({
20
56
  open,
21
57
  content,
22
58
  onClose,
59
+ allCorrections,
60
+ referenceLyrics
23
61
  }: DetailsModalProps) {
24
62
  if (!content) return null
25
63
 
64
+ const referenceWordsText = formatReferenceWords(content, referenceLyrics)
65
+ const relevantCorrections = content.type === 'gap' ? allCorrections.filter(c =>
66
+ c.word_id === content.data.wordId ||
67
+ c.corrected_word_id === content.data.wordId ||
68
+ content.data.transcribed_word_ids.includes(c.word_id)
69
+ ) : []
70
+ const isCorrected = content.type === 'gap' && relevantCorrections.length > 0
71
+
26
72
  const getCurrentWord = () => {
27
73
  if (content.type === 'gap') {
28
74
  return content.data.word
29
75
  } else if (content.type === 'anchor') {
30
- return content.data.word ?? content.data.words[0]
76
+ return content.data.word ?? ''
31
77
  }
32
78
  return ''
33
79
  }
34
80
 
35
- const isCorrected = content.type === 'gap' && content.data.corrections?.length > 0
36
-
37
81
  const renderContent = () => {
82
+ const anchorWords = content.type === 'anchor' ? content.data.transcribed_word_ids?.length ?? 0 : 0
83
+
38
84
  switch (content.type) {
39
85
  case 'anchor':
40
86
  return (
@@ -45,16 +91,17 @@ export default function DetailsModal({
45
91
  />
46
92
  <GridItem
47
93
  title="Full Text"
48
- value={`"${content.data.text}"`}
94
+ value={`"${content.data.word ?? ''}"`}
49
95
  />
50
96
  <GridItem title="Word ID" value={content.data.wordId} />
51
- <GridItem title="Length" value={`${content.data.length} words`} />
52
97
  <GridItem
53
- title="Reference Word IDs"
54
- value={content.data.reference_word_ids ?
55
- Object.entries(content.data.reference_word_ids).map(([source, ids]) => (
56
- `${source}: ${ids?.join(', ') ?? 'No IDs'}`
57
- )).join('\n') : 'No reference word IDs'
98
+ title="Length"
99
+ value={`${anchorWords} words`}
100
+ />
101
+ <GridItem
102
+ title="Reference Words"
103
+ value={
104
+ <pre style={{ margin: 0, whiteSpace: 'pre-wrap' }}>{referenceWordsText}</pre>
58
105
  }
59
106
  />
60
107
  <GridItem title="Confidence" value={content.data.confidence?.toFixed(3) ?? 'N/A'} />
@@ -82,66 +129,45 @@ export default function DetailsModal({
82
129
  title="Selected Word"
83
130
  value={`"${getCurrentWord()}"`}
84
131
  />
85
- <GridItem
86
- title="Current Text"
87
- value={`"${content.data.words?.map(word => {
88
- const wordCorrection = content.data.corrections?.find(
89
- c => c.original_word === word
90
- )
91
- return wordCorrection ? wordCorrection.corrected_word : word
92
- }).join(' ') ?? content.data.word}"`}
93
- />
94
132
  <GridItem
95
133
  title="Word ID"
96
134
  value={content.data.wordId}
97
135
  />
98
136
  <GridItem
99
137
  title="Length"
100
- value={`${content.data.length} words`}
138
+ value={`${content.data.transcribed_word_ids?.length ?? 0} words`}
101
139
  />
102
- {content.data.preceding_anchor && (
140
+ {content.data.preceding_anchor_id && (
103
141
  <GridItem
104
142
  title="Preceding Anchor"
105
- value={`"${content.data.preceding_anchor.text}"`}
143
+ value={getAnchorText(content.data.preceding_anchor_id, content.data.anchor_sequences, referenceLyrics)}
106
144
  />
107
145
  )}
108
- <GridItem
109
- title="Transcribed Text"
110
- value={`"${content.data.text}"`}
111
- />
112
- {content.data.following_anchor && (
146
+ {content.data.following_anchor_id && (
113
147
  <GridItem
114
148
  title="Following Anchor"
115
- value={`"${content.data.following_anchor.text}"`}
149
+ value={getAnchorText(content.data.following_anchor_id, content.data.anchor_sequences, referenceLyrics)}
116
150
  />
117
151
  )}
118
- {content.data.reference_words && (
119
- <GridItem
120
- title="Reference Words"
121
- value={
122
- <>
123
- {content.data.reference_words.spotify && (
124
- <Typography>Spotify: "{content.data.reference_words.spotify.join(' ')}"</Typography>
125
- )}
126
- {content.data.reference_words.genius && (
127
- <Typography>Genius: "{content.data.reference_words.genius.join(' ')}"</Typography>
128
- )}
129
- </>
130
- }
131
- />
132
- )}
133
- {isCorrected && content.data.corrections && (
152
+ <GridItem
153
+ title="Reference Words"
154
+ value={
155
+ <pre style={{ margin: 0, whiteSpace: 'pre-wrap' }}>{referenceWordsText}</pre>
156
+ }
157
+ />
158
+ {isCorrected && (
134
159
  <GridItem
135
160
  title="Correction Details"
136
161
  value={
137
162
  <>
138
- {content.data.corrections.map((correction, index) => (
163
+ {relevantCorrections.map((correction, index) => (
139
164
  <Box key={index} sx={{ mb: 2, p: 1, border: '1px solid #ccc', borderRadius: '4px' }}>
140
165
  <Typography variant="subtitle2" fontWeight="bold">Correction {index + 1}</Typography>
141
166
  <Typography>Original: <strong>"{correction.original_word}"</strong></Typography>
142
167
  <Typography>Corrected: <strong>"{correction.corrected_word}"</strong></Typography>
143
168
  <Typography>Word ID: {correction.word_id}</Typography>
144
169
  <Typography>Confidence: {correction.confidence?.toFixed(3) ?? 'N/A'}</Typography>
170
+ <Typography>Handler: {correction.handler}</Typography>
145
171
  <Typography>Source: {correction.source}</Typography>
146
172
  <Typography>Reason: {correction.reason}</Typography>
147
173
  {correction.is_deletion && <Typography>Is Deletion: Yes</Typography>}
@@ -154,7 +180,7 @@ export default function DetailsModal({
154
180
  <Box sx={{ pl: 2 }}>
155
181
  {Object.entries(correction.alternatives).map(([word, score]) => (
156
182
  <Typography key={word}>
157
- "{word}": {score?.toFixed(3) ?? 'N/A'}
183
+ "{word}": {(score || 0).toFixed(3)}
158
184
  </Typography>
159
185
  ))}
160
186
  </Box>
@@ -178,24 +204,25 @@ export default function DetailsModal({
178
204
  <Dialog
179
205
  open={open}
180
206
  onClose={onClose}
181
- maxWidth="sm"
207
+ maxWidth="md"
182
208
  fullWidth
183
209
  >
184
- <IconButton
185
- onClick={onClose}
186
- sx={{
187
- position: 'absolute',
188
- right: 8,
189
- top: 8,
190
- }}
191
- >
192
- <CloseIcon />
193
- </IconButton>
194
210
  <DialogTitle>
195
- {content.type === 'gap' && (isCorrected ? 'Corrected ' : 'Uncorrected ')}
211
+ {content.type === 'gap' && (
212
+ isCorrected ? 'Corrected ' : 'Uncorrected '
213
+ )}
196
214
  {content.type.charAt(0).toUpperCase() + content.type.slice(1)} Details - "{getCurrentWord()}"
215
+ <IconButton
216
+ aria-label="close"
217
+ onClick={onClose}
218
+ sx={{ position: 'absolute', right: 8, top: 8 }}
219
+ >
220
+ <CloseIcon />
221
+ </IconButton>
197
222
  </DialogTitle>
198
- <DialogContent dividers>{renderContent()}</DialogContent>
223
+ <DialogContent>
224
+ {renderContent()}
225
+ </DialogContent>
199
226
  </Dialog>
200
227
  )
201
228
  }