karaoke-gen 0.57.0__py3-none-any.whl → 0.71.27__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 (268) hide show
  1. karaoke_gen/audio_fetcher.py +461 -0
  2. karaoke_gen/audio_processor.py +407 -30
  3. karaoke_gen/config.py +62 -113
  4. karaoke_gen/file_handler.py +32 -59
  5. karaoke_gen/karaoke_finalise/karaoke_finalise.py +148 -67
  6. karaoke_gen/karaoke_gen.py +270 -61
  7. karaoke_gen/lyrics_processor.py +13 -1
  8. karaoke_gen/metadata.py +78 -73
  9. karaoke_gen/pipeline/__init__.py +87 -0
  10. karaoke_gen/pipeline/base.py +215 -0
  11. karaoke_gen/pipeline/context.py +230 -0
  12. karaoke_gen/pipeline/executors/__init__.py +21 -0
  13. karaoke_gen/pipeline/executors/local.py +159 -0
  14. karaoke_gen/pipeline/executors/remote.py +257 -0
  15. karaoke_gen/pipeline/stages/__init__.py +27 -0
  16. karaoke_gen/pipeline/stages/finalize.py +202 -0
  17. karaoke_gen/pipeline/stages/render.py +165 -0
  18. karaoke_gen/pipeline/stages/screens.py +139 -0
  19. karaoke_gen/pipeline/stages/separation.py +191 -0
  20. karaoke_gen/pipeline/stages/transcription.py +191 -0
  21. karaoke_gen/style_loader.py +531 -0
  22. karaoke_gen/utils/bulk_cli.py +6 -0
  23. karaoke_gen/utils/cli_args.py +424 -0
  24. karaoke_gen/utils/gen_cli.py +26 -261
  25. karaoke_gen/utils/remote_cli.py +1965 -0
  26. karaoke_gen/video_background_processor.py +351 -0
  27. karaoke_gen-0.71.27.dist-info/METADATA +610 -0
  28. karaoke_gen-0.71.27.dist-info/RECORD +275 -0
  29. {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.27.dist-info}/WHEEL +1 -1
  30. {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.27.dist-info}/entry_points.txt +1 -0
  31. lyrics_transcriber/__init__.py +10 -0
  32. lyrics_transcriber/cli/__init__.py +0 -0
  33. lyrics_transcriber/cli/cli_main.py +285 -0
  34. lyrics_transcriber/core/__init__.py +0 -0
  35. lyrics_transcriber/core/config.py +50 -0
  36. lyrics_transcriber/core/controller.py +520 -0
  37. lyrics_transcriber/correction/__init__.py +0 -0
  38. lyrics_transcriber/correction/agentic/__init__.py +9 -0
  39. lyrics_transcriber/correction/agentic/adapter.py +71 -0
  40. lyrics_transcriber/correction/agentic/agent.py +313 -0
  41. lyrics_transcriber/correction/agentic/feedback/aggregator.py +12 -0
  42. lyrics_transcriber/correction/agentic/feedback/collector.py +17 -0
  43. lyrics_transcriber/correction/agentic/feedback/retention.py +24 -0
  44. lyrics_transcriber/correction/agentic/feedback/store.py +76 -0
  45. lyrics_transcriber/correction/agentic/handlers/__init__.py +24 -0
  46. lyrics_transcriber/correction/agentic/handlers/ambiguous.py +44 -0
  47. lyrics_transcriber/correction/agentic/handlers/background_vocals.py +68 -0
  48. lyrics_transcriber/correction/agentic/handlers/base.py +51 -0
  49. lyrics_transcriber/correction/agentic/handlers/complex_multi_error.py +46 -0
  50. lyrics_transcriber/correction/agentic/handlers/extra_words.py +74 -0
  51. lyrics_transcriber/correction/agentic/handlers/no_error.py +42 -0
  52. lyrics_transcriber/correction/agentic/handlers/punctuation.py +44 -0
  53. lyrics_transcriber/correction/agentic/handlers/registry.py +60 -0
  54. lyrics_transcriber/correction/agentic/handlers/repeated_section.py +44 -0
  55. lyrics_transcriber/correction/agentic/handlers/sound_alike.py +126 -0
  56. lyrics_transcriber/correction/agentic/models/__init__.py +5 -0
  57. lyrics_transcriber/correction/agentic/models/ai_correction.py +31 -0
  58. lyrics_transcriber/correction/agentic/models/correction_session.py +30 -0
  59. lyrics_transcriber/correction/agentic/models/enums.py +38 -0
  60. lyrics_transcriber/correction/agentic/models/human_feedback.py +30 -0
  61. lyrics_transcriber/correction/agentic/models/learning_data.py +26 -0
  62. lyrics_transcriber/correction/agentic/models/observability_metrics.py +28 -0
  63. lyrics_transcriber/correction/agentic/models/schemas.py +46 -0
  64. lyrics_transcriber/correction/agentic/models/utils.py +19 -0
  65. lyrics_transcriber/correction/agentic/observability/__init__.py +5 -0
  66. lyrics_transcriber/correction/agentic/observability/langfuse_integration.py +35 -0
  67. lyrics_transcriber/correction/agentic/observability/metrics.py +46 -0
  68. lyrics_transcriber/correction/agentic/observability/performance.py +19 -0
  69. lyrics_transcriber/correction/agentic/prompts/__init__.py +2 -0
  70. lyrics_transcriber/correction/agentic/prompts/classifier.py +227 -0
  71. lyrics_transcriber/correction/agentic/providers/__init__.py +6 -0
  72. lyrics_transcriber/correction/agentic/providers/base.py +36 -0
  73. lyrics_transcriber/correction/agentic/providers/circuit_breaker.py +145 -0
  74. lyrics_transcriber/correction/agentic/providers/config.py +73 -0
  75. lyrics_transcriber/correction/agentic/providers/constants.py +24 -0
  76. lyrics_transcriber/correction/agentic/providers/health.py +28 -0
  77. lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +212 -0
  78. lyrics_transcriber/correction/agentic/providers/model_factory.py +209 -0
  79. lyrics_transcriber/correction/agentic/providers/response_cache.py +218 -0
  80. lyrics_transcriber/correction/agentic/providers/response_parser.py +111 -0
  81. lyrics_transcriber/correction/agentic/providers/retry_executor.py +127 -0
  82. lyrics_transcriber/correction/agentic/router.py +35 -0
  83. lyrics_transcriber/correction/agentic/workflows/__init__.py +5 -0
  84. lyrics_transcriber/correction/agentic/workflows/consensus_workflow.py +24 -0
  85. lyrics_transcriber/correction/agentic/workflows/correction_graph.py +59 -0
  86. lyrics_transcriber/correction/agentic/workflows/feedback_workflow.py +24 -0
  87. lyrics_transcriber/correction/anchor_sequence.py +1043 -0
  88. lyrics_transcriber/correction/corrector.py +760 -0
  89. lyrics_transcriber/correction/feedback/__init__.py +2 -0
  90. lyrics_transcriber/correction/feedback/schemas.py +107 -0
  91. lyrics_transcriber/correction/feedback/store.py +236 -0
  92. lyrics_transcriber/correction/handlers/__init__.py +0 -0
  93. lyrics_transcriber/correction/handlers/base.py +52 -0
  94. lyrics_transcriber/correction/handlers/extend_anchor.py +149 -0
  95. lyrics_transcriber/correction/handlers/levenshtein.py +189 -0
  96. lyrics_transcriber/correction/handlers/llm.py +293 -0
  97. lyrics_transcriber/correction/handlers/llm_providers.py +60 -0
  98. lyrics_transcriber/correction/handlers/no_space_punct_match.py +154 -0
  99. lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +85 -0
  100. lyrics_transcriber/correction/handlers/repeat.py +88 -0
  101. lyrics_transcriber/correction/handlers/sound_alike.py +259 -0
  102. lyrics_transcriber/correction/handlers/syllables_match.py +252 -0
  103. lyrics_transcriber/correction/handlers/word_count_match.py +80 -0
  104. lyrics_transcriber/correction/handlers/word_operations.py +187 -0
  105. lyrics_transcriber/correction/operations.py +352 -0
  106. lyrics_transcriber/correction/phrase_analyzer.py +435 -0
  107. lyrics_transcriber/correction/text_utils.py +30 -0
  108. lyrics_transcriber/frontend/.gitignore +23 -0
  109. lyrics_transcriber/frontend/.yarn/releases/yarn-4.7.0.cjs +935 -0
  110. lyrics_transcriber/frontend/.yarnrc.yml +3 -0
  111. lyrics_transcriber/frontend/README.md +50 -0
  112. lyrics_transcriber/frontend/REPLACE_ALL_FUNCTIONALITY.md +210 -0
  113. lyrics_transcriber/frontend/__init__.py +25 -0
  114. lyrics_transcriber/frontend/eslint.config.js +28 -0
  115. lyrics_transcriber/frontend/index.html +18 -0
  116. lyrics_transcriber/frontend/package.json +42 -0
  117. lyrics_transcriber/frontend/public/android-chrome-192x192.png +0 -0
  118. lyrics_transcriber/frontend/public/android-chrome-512x512.png +0 -0
  119. lyrics_transcriber/frontend/public/apple-touch-icon.png +0 -0
  120. lyrics_transcriber/frontend/public/favicon-16x16.png +0 -0
  121. lyrics_transcriber/frontend/public/favicon-32x32.png +0 -0
  122. lyrics_transcriber/frontend/public/favicon.ico +0 -0
  123. lyrics_transcriber/frontend/public/nomad-karaoke-logo.png +0 -0
  124. lyrics_transcriber/frontend/src/App.tsx +212 -0
  125. lyrics_transcriber/frontend/src/api.ts +239 -0
  126. lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +77 -0
  127. lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx +114 -0
  128. lyrics_transcriber/frontend/src/components/AgenticCorrectionMetrics.tsx +204 -0
  129. lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +180 -0
  130. lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +167 -0
  131. lyrics_transcriber/frontend/src/components/CorrectionAnnotationModal.tsx +359 -0
  132. lyrics_transcriber/frontend/src/components/CorrectionDetailCard.tsx +281 -0
  133. lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +162 -0
  134. lyrics_transcriber/frontend/src/components/DurationTimelineView.tsx +257 -0
  135. lyrics_transcriber/frontend/src/components/EditActionBar.tsx +68 -0
  136. lyrics_transcriber/frontend/src/components/EditModal.tsx +702 -0
  137. lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +496 -0
  138. lyrics_transcriber/frontend/src/components/EditWordList.tsx +379 -0
  139. lyrics_transcriber/frontend/src/components/FileUpload.tsx +77 -0
  140. lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +467 -0
  141. lyrics_transcriber/frontend/src/components/Header.tsx +387 -0
  142. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +1373 -0
  143. lyrics_transcriber/frontend/src/components/MetricsDashboard.tsx +51 -0
  144. lyrics_transcriber/frontend/src/components/ModeSelector.tsx +67 -0
  145. lyrics_transcriber/frontend/src/components/ModelSelector.tsx +23 -0
  146. lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +144 -0
  147. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +268 -0
  148. lyrics_transcriber/frontend/src/components/ReplaceAllLyricsModal.tsx +688 -0
  149. lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +354 -0
  150. lyrics_transcriber/frontend/src/components/SegmentDetailsModal.tsx +64 -0
  151. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +376 -0
  152. lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +131 -0
  153. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +256 -0
  154. lyrics_transcriber/frontend/src/components/WordDivider.tsx +187 -0
  155. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +379 -0
  156. lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +56 -0
  157. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +87 -0
  158. lyrics_transcriber/frontend/src/components/shared/constants.ts +20 -0
  159. lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +180 -0
  160. lyrics_transcriber/frontend/src/components/shared/styles.ts +13 -0
  161. lyrics_transcriber/frontend/src/components/shared/types.js +2 -0
  162. lyrics_transcriber/frontend/src/components/shared/types.ts +129 -0
  163. lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +177 -0
  164. lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +78 -0
  165. lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +75 -0
  166. lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +360 -0
  167. lyrics_transcriber/frontend/src/components/shared/utils/timingUtils.ts +110 -0
  168. lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +22 -0
  169. lyrics_transcriber/frontend/src/hooks/useManualSync.ts +435 -0
  170. lyrics_transcriber/frontend/src/main.tsx +17 -0
  171. lyrics_transcriber/frontend/src/theme.ts +177 -0
  172. lyrics_transcriber/frontend/src/types/global.d.ts +9 -0
  173. lyrics_transcriber/frontend/src/types.js +2 -0
  174. lyrics_transcriber/frontend/src/types.ts +199 -0
  175. lyrics_transcriber/frontend/src/validation.ts +132 -0
  176. lyrics_transcriber/frontend/src/vite-env.d.ts +1 -0
  177. lyrics_transcriber/frontend/tsconfig.app.json +26 -0
  178. lyrics_transcriber/frontend/tsconfig.json +25 -0
  179. lyrics_transcriber/frontend/tsconfig.node.json +23 -0
  180. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -0
  181. lyrics_transcriber/frontend/update_version.js +11 -0
  182. lyrics_transcriber/frontend/vite.config.d.ts +2 -0
  183. lyrics_transcriber/frontend/vite.config.js +10 -0
  184. lyrics_transcriber/frontend/vite.config.ts +11 -0
  185. lyrics_transcriber/frontend/web_assets/android-chrome-192x192.png +0 -0
  186. lyrics_transcriber/frontend/web_assets/android-chrome-512x512.png +0 -0
  187. lyrics_transcriber/frontend/web_assets/apple-touch-icon.png +0 -0
  188. lyrics_transcriber/frontend/web_assets/assets/index-DdJTDWH3.js +42039 -0
  189. lyrics_transcriber/frontend/web_assets/assets/index-DdJTDWH3.js.map +1 -0
  190. lyrics_transcriber/frontend/web_assets/favicon-16x16.png +0 -0
  191. lyrics_transcriber/frontend/web_assets/favicon-32x32.png +0 -0
  192. lyrics_transcriber/frontend/web_assets/favicon.ico +0 -0
  193. lyrics_transcriber/frontend/web_assets/index.html +18 -0
  194. lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.png +0 -0
  195. lyrics_transcriber/frontend/yarn.lock +3752 -0
  196. lyrics_transcriber/lyrics/__init__.py +0 -0
  197. lyrics_transcriber/lyrics/base_lyrics_provider.py +211 -0
  198. lyrics_transcriber/lyrics/file_provider.py +95 -0
  199. lyrics_transcriber/lyrics/genius.py +384 -0
  200. lyrics_transcriber/lyrics/lrclib.py +231 -0
  201. lyrics_transcriber/lyrics/musixmatch.py +156 -0
  202. lyrics_transcriber/lyrics/spotify.py +290 -0
  203. lyrics_transcriber/lyrics/user_input_provider.py +44 -0
  204. lyrics_transcriber/output/__init__.py +0 -0
  205. lyrics_transcriber/output/ass/__init__.py +21 -0
  206. lyrics_transcriber/output/ass/ass.py +2088 -0
  207. lyrics_transcriber/output/ass/ass_specs.txt +732 -0
  208. lyrics_transcriber/output/ass/config.py +180 -0
  209. lyrics_transcriber/output/ass/constants.py +23 -0
  210. lyrics_transcriber/output/ass/event.py +94 -0
  211. lyrics_transcriber/output/ass/formatters.py +132 -0
  212. lyrics_transcriber/output/ass/lyrics_line.py +265 -0
  213. lyrics_transcriber/output/ass/lyrics_screen.py +252 -0
  214. lyrics_transcriber/output/ass/section_detector.py +89 -0
  215. lyrics_transcriber/output/ass/section_screen.py +106 -0
  216. lyrics_transcriber/output/ass/style.py +187 -0
  217. lyrics_transcriber/output/cdg.py +619 -0
  218. lyrics_transcriber/output/cdgmaker/__init__.py +0 -0
  219. lyrics_transcriber/output/cdgmaker/cdg.py +262 -0
  220. lyrics_transcriber/output/cdgmaker/composer.py +2260 -0
  221. lyrics_transcriber/output/cdgmaker/config.py +151 -0
  222. lyrics_transcriber/output/cdgmaker/images/instrumental.png +0 -0
  223. lyrics_transcriber/output/cdgmaker/images/intro.png +0 -0
  224. lyrics_transcriber/output/cdgmaker/pack.py +507 -0
  225. lyrics_transcriber/output/cdgmaker/render.py +346 -0
  226. lyrics_transcriber/output/cdgmaker/transitions/centertexttoplogobottomtext.png +0 -0
  227. lyrics_transcriber/output/cdgmaker/transitions/circlein.png +0 -0
  228. lyrics_transcriber/output/cdgmaker/transitions/circleout.png +0 -0
  229. lyrics_transcriber/output/cdgmaker/transitions/fizzle.png +0 -0
  230. lyrics_transcriber/output/cdgmaker/transitions/largecentertexttoplogo.png +0 -0
  231. lyrics_transcriber/output/cdgmaker/transitions/rectangle.png +0 -0
  232. lyrics_transcriber/output/cdgmaker/transitions/spiral.png +0 -0
  233. lyrics_transcriber/output/cdgmaker/transitions/topleftmusicalnotes.png +0 -0
  234. lyrics_transcriber/output/cdgmaker/transitions/wipein.png +0 -0
  235. lyrics_transcriber/output/cdgmaker/transitions/wipeleft.png +0 -0
  236. lyrics_transcriber/output/cdgmaker/transitions/wipeout.png +0 -0
  237. lyrics_transcriber/output/cdgmaker/transitions/wiperight.png +0 -0
  238. lyrics_transcriber/output/cdgmaker/utils.py +132 -0
  239. lyrics_transcriber/output/countdown_processor.py +267 -0
  240. lyrics_transcriber/output/fonts/AvenirNext-Bold.ttf +0 -0
  241. lyrics_transcriber/output/fonts/DMSans-VariableFont_opsz,wght.ttf +0 -0
  242. lyrics_transcriber/output/fonts/DMSerifDisplay-Regular.ttf +0 -0
  243. lyrics_transcriber/output/fonts/Oswald-SemiBold.ttf +0 -0
  244. lyrics_transcriber/output/fonts/Zurich_Cn_BT_Bold.ttf +0 -0
  245. lyrics_transcriber/output/fonts/arial.ttf +0 -0
  246. lyrics_transcriber/output/fonts/georgia.ttf +0 -0
  247. lyrics_transcriber/output/fonts/verdana.ttf +0 -0
  248. lyrics_transcriber/output/generator.py +257 -0
  249. lyrics_transcriber/output/lrc_to_cdg.py +61 -0
  250. lyrics_transcriber/output/lyrics_file.py +102 -0
  251. lyrics_transcriber/output/plain_text.py +96 -0
  252. lyrics_transcriber/output/segment_resizer.py +431 -0
  253. lyrics_transcriber/output/subtitles.py +397 -0
  254. lyrics_transcriber/output/video.py +544 -0
  255. lyrics_transcriber/review/__init__.py +0 -0
  256. lyrics_transcriber/review/server.py +676 -0
  257. lyrics_transcriber/storage/__init__.py +0 -0
  258. lyrics_transcriber/storage/dropbox.py +225 -0
  259. lyrics_transcriber/transcribers/__init__.py +0 -0
  260. lyrics_transcriber/transcribers/audioshake.py +290 -0
  261. lyrics_transcriber/transcribers/base_transcriber.py +157 -0
  262. lyrics_transcriber/transcribers/whisper.py +330 -0
  263. lyrics_transcriber/types.py +648 -0
  264. lyrics_transcriber/utils/__init__.py +0 -0
  265. lyrics_transcriber/utils/word_utils.py +27 -0
  266. karaoke_gen-0.57.0.dist-info/METADATA +0 -167
  267. karaoke_gen-0.57.0.dist-info/RECORD +0 -23
  268. {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.27.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,1373 @@
1
+ import { useState, useEffect, useCallback, useMemo, memo } from 'react'
2
+ import {
3
+ AnchorSequence,
4
+ CorrectionData,
5
+ GapSequence,
6
+ HighlightInfo,
7
+ InteractionMode,
8
+ LyricsSegment,
9
+ ReferenceSource,
10
+ WordCorrection,
11
+ CorrectionAnnotation
12
+ } from '../types'
13
+ import { Box, Button, Grid, useMediaQuery, useTheme } from '@mui/material'
14
+ import { ApiClient } from '../api'
15
+ import ReferenceView from './ReferenceView'
16
+ import TranscriptionView from './TranscriptionView'
17
+ import { WordClickInfo, FlashType } from './shared/types'
18
+ import EditModal from './EditModal'
19
+ import ReviewChangesModal from './ReviewChangesModal'
20
+ import ReplaceAllLyricsModal from './ReplaceAllLyricsModal'
21
+ import CorrectionAnnotationModal from './CorrectionAnnotationModal'
22
+ import CorrectionDetailCard from './CorrectionDetailCard'
23
+ import {
24
+ addSegmentBefore,
25
+ splitSegment,
26
+ deleteSegment,
27
+ mergeSegment,
28
+ findAndReplace,
29
+ deleteWord
30
+ } from './shared/utils/segmentOperations'
31
+ import { loadSavedData, saveData, clearSavedData } from './shared/utils/localStorage'
32
+ import { setupKeyboardHandlers, setModalHandler, getModalState } from './shared/utils/keyboardHandlers'
33
+ import Header from './Header'
34
+ import { getWordsFromIds } from './shared/utils/wordUtils'
35
+ import AddLyricsModal from './AddLyricsModal'
36
+ import { RestoreFromTrash, OndemandVideo } from '@mui/icons-material'
37
+ import FindReplaceModal from './FindReplaceModal'
38
+ import TimingOffsetModal from './TimingOffsetModal'
39
+ import { applyOffsetToCorrectionData, applyOffsetToSegment } from './shared/utils/timingUtils'
40
+
41
+ // Add type for window augmentation at the top of the file
42
+ declare global {
43
+ interface Window {
44
+ toggleAudioPlayback?: () => void;
45
+ seekAndPlayAudio?: (startTime: number) => void;
46
+ getAudioDuration?: () => number;
47
+ isAudioPlaying?: boolean;
48
+ }
49
+ }
50
+
51
+ const debugLog = false;
52
+ export interface LyricsAnalyzerProps {
53
+ data: CorrectionData
54
+ onFileLoad: () => void
55
+ onShowMetadata: () => void
56
+ apiClient: ApiClient | null
57
+ isReadOnly: boolean
58
+ audioHash: string
59
+ }
60
+
61
+ export type ModalContent = {
62
+ type: 'anchor'
63
+ data: AnchorSequence & {
64
+ wordId: string
65
+ word?: string
66
+ anchor_sequences: AnchorSequence[]
67
+ }
68
+ } | {
69
+ type: 'gap'
70
+ data: GapSequence & {
71
+ wordId: string
72
+ word: string
73
+ anchor_sequences: AnchorSequence[]
74
+ }
75
+ }
76
+
77
+ // Define types for the memoized components
78
+ interface MemoizedTranscriptionViewProps {
79
+ data: CorrectionData
80
+ mode: InteractionMode
81
+ onElementClick: (content: ModalContent) => void
82
+ onWordClick: (info: WordClickInfo) => void
83
+ flashingType: FlashType
84
+ flashingHandler: string | null
85
+ highlightInfo: HighlightInfo | null
86
+ onPlaySegment?: (time: number) => void
87
+ currentTime: number
88
+ anchors: AnchorSequence[]
89
+ disableHighlighting: boolean
90
+ onDataChange?: (updatedData: CorrectionData) => void
91
+ }
92
+
93
+ // Create a memoized TranscriptionView component
94
+ const MemoizedTranscriptionView = memo(function MemoizedTranscriptionView({
95
+ data,
96
+ mode,
97
+ onElementClick,
98
+ onWordClick,
99
+ flashingType,
100
+ flashingHandler,
101
+ highlightInfo,
102
+ onPlaySegment,
103
+ currentTime,
104
+ anchors,
105
+ disableHighlighting,
106
+ onDataChange
107
+ }: MemoizedTranscriptionViewProps) {
108
+ return (
109
+ <TranscriptionView
110
+ data={data}
111
+ mode={mode}
112
+ onElementClick={onElementClick}
113
+ onWordClick={onWordClick}
114
+ flashingType={flashingType}
115
+ flashingHandler={flashingHandler}
116
+ highlightInfo={highlightInfo}
117
+ onPlaySegment={onPlaySegment}
118
+ currentTime={disableHighlighting ? undefined : currentTime}
119
+ anchors={anchors}
120
+ onDataChange={onDataChange}
121
+ />
122
+ );
123
+ });
124
+
125
+ interface MemoizedReferenceViewProps {
126
+ referenceSources: Record<string, ReferenceSource>
127
+ anchors: AnchorSequence[]
128
+ gaps: GapSequence[]
129
+ mode: InteractionMode
130
+ onElementClick: (content: ModalContent) => void
131
+ onWordClick: (info: WordClickInfo) => void
132
+ flashingType: FlashType
133
+ highlightInfo: HighlightInfo | null
134
+ currentSource: string
135
+ onSourceChange: (source: string) => void
136
+ corrected_segments: LyricsSegment[]
137
+ corrections: WordCorrection[]
138
+ onAddLyrics?: () => void
139
+ }
140
+
141
+ // Create a memoized ReferenceView component
142
+ const MemoizedReferenceView = memo(function MemoizedReferenceView({
143
+ referenceSources,
144
+ anchors,
145
+ gaps,
146
+ mode,
147
+ onElementClick,
148
+ onWordClick,
149
+ flashingType,
150
+ highlightInfo,
151
+ currentSource,
152
+ onSourceChange,
153
+ corrected_segments,
154
+ corrections,
155
+ onAddLyrics
156
+ }: MemoizedReferenceViewProps) {
157
+ return (
158
+ <ReferenceView
159
+ referenceSources={referenceSources}
160
+ anchors={anchors}
161
+ gaps={gaps}
162
+ mode={mode}
163
+ onElementClick={onElementClick}
164
+ onWordClick={onWordClick}
165
+ flashingType={flashingType}
166
+ highlightInfo={highlightInfo}
167
+ currentSource={currentSource}
168
+ onSourceChange={onSourceChange}
169
+ corrected_segments={corrected_segments}
170
+ corrections={corrections}
171
+ onAddLyrics={onAddLyrics}
172
+ />
173
+ );
174
+ });
175
+
176
+ interface MemoizedHeaderProps {
177
+ isReadOnly: boolean
178
+ onFileLoad: () => void
179
+ data: CorrectionData
180
+ onMetricClick: {
181
+ anchor: () => void
182
+ corrected: () => void
183
+ uncorrected: () => void
184
+ }
185
+ effectiveMode: InteractionMode
186
+ onModeChange: (mode: InteractionMode) => void
187
+ apiClient: ApiClient | null
188
+ audioHash: string
189
+ onTimeUpdate: (time: number) => void
190
+ onHandlerToggle: (handler: string, enabled: boolean) => void
191
+ isUpdatingHandlers: boolean
192
+ onHandlerClick?: (handler: string) => void
193
+ onAddLyrics?: () => void
194
+ onFindReplace?: () => void
195
+ onEditAll?: () => void
196
+ onTimingOffset: () => void
197
+ timingOffsetMs: number
198
+ onUndo: () => void
199
+ onRedo: () => void
200
+ canUndo: boolean
201
+ canRedo: boolean
202
+ onUnCorrectAll: () => void
203
+ }
204
+
205
+ // Create a memoized Header component
206
+ const MemoizedHeader = memo(function MemoizedHeader({
207
+ isReadOnly,
208
+ onFileLoad,
209
+ data,
210
+ onMetricClick,
211
+ effectiveMode,
212
+ onModeChange,
213
+ apiClient,
214
+ audioHash,
215
+ onTimeUpdate,
216
+ onHandlerToggle,
217
+ isUpdatingHandlers,
218
+ onHandlerClick,
219
+ onFindReplace,
220
+ onEditAll,
221
+ onTimingOffset,
222
+ timingOffsetMs,
223
+ onUndo,
224
+ onRedo,
225
+ canUndo,
226
+ canRedo,
227
+ onUnCorrectAll
228
+ }: MemoizedHeaderProps) {
229
+ return (
230
+ <Header
231
+ isReadOnly={isReadOnly}
232
+ onFileLoad={onFileLoad}
233
+ data={data}
234
+ onMetricClick={onMetricClick}
235
+ effectiveMode={effectiveMode}
236
+ onModeChange={onModeChange}
237
+ apiClient={apiClient}
238
+ audioHash={audioHash}
239
+ onTimeUpdate={onTimeUpdate}
240
+ onHandlerToggle={onHandlerToggle}
241
+ isUpdatingHandlers={isUpdatingHandlers}
242
+ onHandlerClick={onHandlerClick}
243
+ onFindReplace={onFindReplace}
244
+ onEditAll={onEditAll}
245
+ onTimingOffset={onTimingOffset}
246
+ timingOffsetMs={timingOffsetMs}
247
+ onUndo={onUndo}
248
+ onRedo={onRedo}
249
+ canUndo={canUndo}
250
+ canRedo={canRedo}
251
+ onUnCorrectAll={onUnCorrectAll}
252
+ />
253
+ );
254
+ });
255
+
256
+ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly, audioHash }: LyricsAnalyzerProps) {
257
+ const [modalContent, setModalContent] = useState<ModalContent | null>(null)
258
+ const [flashingType, setFlashingType] = useState<FlashType>(null)
259
+ const [highlightInfo, setHighlightInfo] = useState<HighlightInfo | null>(null)
260
+ const [currentSource, setCurrentSource] = useState<string>(() => {
261
+ if (!initialData?.reference_lyrics) {
262
+ return ''
263
+ }
264
+ const availableSources = Object.keys(initialData.reference_lyrics)
265
+ return availableSources.length > 0 ? availableSources[0] : ''
266
+ })
267
+ const [isReviewComplete, setIsReviewComplete] = useState(false)
268
+ const [originalData] = useState(() => JSON.parse(JSON.stringify(initialData)))
269
+ const [interactionMode, setInteractionMode] = useState<InteractionMode>('edit')
270
+ const [isShiftPressed, setIsShiftPressed] = useState(false)
271
+ const [isCtrlPressed, setIsCtrlPressed] = useState(false)
272
+ const [editModalSegment, setEditModalSegment] = useState<{
273
+ segment: LyricsSegment
274
+ index: number
275
+ originalSegment: LyricsSegment
276
+ } | null>(null)
277
+ const [isReplaceAllLyricsModalOpen, setIsReplaceAllLyricsModalOpen] = useState(false)
278
+ const [isReviewModalOpen, setIsReviewModalOpen] = useState(false)
279
+ const [currentAudioTime, setCurrentAudioTime] = useState(0)
280
+ const [isUpdatingHandlers, setIsUpdatingHandlers] = useState(false)
281
+ const [flashingHandler, setFlashingHandler] = useState<string | null>(null)
282
+ const [isAddingLyrics, setIsAddingLyrics] = useState(false)
283
+ const [isAddLyricsModalOpen, setIsAddLyricsModalOpen] = useState(false)
284
+ const [isAnyModalOpen, setIsAnyModalOpen] = useState(false)
285
+ const [isFindReplaceModalOpen, setIsFindReplaceModalOpen] = useState(false)
286
+ const [isTimingOffsetModalOpen, setIsTimingOffsetModalOpen] = useState(false)
287
+ const [timingOffsetMs, setTimingOffsetMs] = useState(0)
288
+
289
+ // Annotation collection state
290
+ const [annotations, setAnnotations] = useState<Omit<CorrectionAnnotation, 'annotation_id' | 'timestamp'>[]>([])
291
+ const [isAnnotationModalOpen, setIsAnnotationModalOpen] = useState(false)
292
+ const [pendingAnnotation, setPendingAnnotation] = useState<{
293
+ originalText: string
294
+ correctedText: string
295
+ wordIdsAffected: string[]
296
+ gapId?: string
297
+ } | null>(null)
298
+ const [annotationsEnabled] = useState(() => {
299
+ // Check localStorage for user preference
300
+ const saved = localStorage.getItem('annotationsEnabled')
301
+ return saved !== null ? saved === 'true' : true // Default: enabled
302
+ // TODO: Add UI toggle to enable/disable via setAnnotationsEnabled
303
+ })
304
+
305
+ // Correction detail card state
306
+ const [correctionDetailOpen, setCorrectionDetailOpen] = useState(false)
307
+ const [selectedCorrection, setSelectedCorrection] = useState<{
308
+ wordId: string
309
+ originalWord: string
310
+ correctedWord: string
311
+ category: string | null
312
+ confidence: number
313
+ reason: string
314
+ handler: string
315
+ source: string
316
+ } | null>(null)
317
+
318
+ const theme = useTheme()
319
+ const isMobile = useMediaQuery(theme.breakpoints.down('md'))
320
+
321
+ // State history for Undo/Redo
322
+ const [history, setHistory] = useState<CorrectionData[]>([initialData])
323
+ const [historyIndex, setHistoryIndex] = useState(0)
324
+
325
+ // Derived state: the current data based on history index
326
+ const data = history[historyIndex];
327
+
328
+ // Function to update data and manage history
329
+ const updateDataWithHistory = useCallback((newData: CorrectionData, actionDescription?: string) => {
330
+ if (debugLog) {
331
+ console.log(`[DEBUG] updateDataWithHistory: Action - ${actionDescription || 'Unknown'}. Current index: ${historyIndex}, History length: ${history.length}`);
332
+ }
333
+ const newHistory = history.slice(0, historyIndex + 1)
334
+ const deepCopiedNewData = JSON.parse(JSON.stringify(newData));
335
+
336
+ newHistory.push(deepCopiedNewData)
337
+ setHistory(newHistory)
338
+ setHistoryIndex(newHistory.length - 1)
339
+ if (debugLog) {
340
+ console.log(`[DEBUG] updateDataWithHistory: History updated. New index: ${newHistory.length - 1}, New length: ${newHistory.length}`);
341
+ }
342
+ }, [history, historyIndex])
343
+
344
+ // Reset history when initial data changes (e.g., new file loaded)
345
+ useEffect(() => {
346
+ setHistory([initialData])
347
+ setHistoryIndex(0)
348
+ }, [initialData])
349
+
350
+ // Update debug logging to use new ID-based structure
351
+ useEffect(() => {
352
+ if (debugLog) {
353
+ console.log('LyricsAnalyzer Initial Data:', {
354
+ hasData: !!initialData,
355
+ segmentsCount: initialData?.corrected_segments?.length ?? 0,
356
+ anchorsCount: initialData?.anchor_sequences?.length ?? 0,
357
+ gapsCount: initialData?.gap_sequences?.length ?? 0,
358
+ firstAnchor: initialData?.anchor_sequences?.[0] && {
359
+ transcribedWordIds: initialData.anchor_sequences[0].transcribed_word_ids,
360
+ referenceWordIds: initialData.anchor_sequences[0].reference_word_ids
361
+ },
362
+ firstSegment: initialData?.corrected_segments?.[0],
363
+ referenceSources: Object.keys(initialData?.reference_lyrics ?? {})
364
+ });
365
+ }
366
+ }, [initialData]);
367
+
368
+ // Load saved data
369
+ useEffect(() => {
370
+ const savedData = loadSavedData(initialData)
371
+ if (savedData && window.confirm('Found saved progress for this song. Would you like to restore it?')) {
372
+ // Replace history with saved data as the initial state
373
+ setHistory([savedData])
374
+ setHistoryIndex(0)
375
+ }
376
+ }, [initialData]) // Keep dependency only on initialData
377
+
378
+ // Save data - This should save the *current* state, not affect history
379
+ useEffect(() => {
380
+ if (!isReadOnly) {
381
+ saveData(data, initialData) // Use 'data' derived from history and the initialData prop
382
+ }
383
+ }, [data, isReadOnly, initialData]) // Correct dependencies
384
+
385
+ // Keyboard handlers
386
+ useEffect(() => {
387
+ const { currentModalHandler } = getModalState()
388
+
389
+ if (debugLog) {
390
+ console.log('LyricsAnalyzer - Setting up keyboard effect', {
391
+ isAnyModalOpen,
392
+ hasSpacebarHandler: !!currentModalHandler
393
+ })
394
+ }
395
+
396
+ const { handleKeyDown, handleKeyUp, cleanup } = setupKeyboardHandlers({
397
+ setIsShiftPressed,
398
+ setIsCtrlPressed
399
+ })
400
+
401
+ // Always add keyboard listeners
402
+ if (debugLog) {
403
+ console.log('LyricsAnalyzer - Adding keyboard event listeners')
404
+ }
405
+ window.addEventListener('keydown', handleKeyDown)
406
+ window.addEventListener('keyup', handleKeyUp)
407
+
408
+ // Reset modifier states when a modal opens
409
+ if (isAnyModalOpen) {
410
+ setIsShiftPressed(false)
411
+ setIsCtrlPressed(false)
412
+ }
413
+
414
+ // Cleanup function
415
+ return () => {
416
+ if (debugLog) {
417
+ console.log('LyricsAnalyzer - Cleanup effect running')
418
+ }
419
+ window.removeEventListener('keydown', handleKeyDown)
420
+ window.removeEventListener('keyup', handleKeyUp)
421
+ document.body.style.userSelect = ''
422
+ // Call the cleanup function to remove window blur/focus listeners
423
+ cleanup()
424
+ }
425
+ }, [setIsShiftPressed, setIsCtrlPressed, isAnyModalOpen])
426
+
427
+ // Update modal state tracking
428
+ useEffect(() => {
429
+ const modalOpen = Boolean(
430
+ modalContent ||
431
+ editModalSegment ||
432
+ isReviewModalOpen ||
433
+ isAddLyricsModalOpen ||
434
+ isFindReplaceModalOpen ||
435
+ isReplaceAllLyricsModalOpen ||
436
+ isTimingOffsetModalOpen
437
+ )
438
+ setIsAnyModalOpen(modalOpen)
439
+ }, [modalContent, editModalSegment, isReviewModalOpen, isAddLyricsModalOpen, isFindReplaceModalOpen, isReplaceAllLyricsModalOpen, isTimingOffsetModalOpen])
440
+
441
+ // Calculate effective mode based on modifier key states
442
+ const effectiveMode = isCtrlPressed ? 'delete_word' : (isShiftPressed ? 'highlight' : interactionMode)
443
+
444
+ const handleFlash = useCallback((type: FlashType, info?: HighlightInfo) => {
445
+ setFlashingType(null)
446
+ setHighlightInfo(null)
447
+
448
+ requestAnimationFrame(() => {
449
+ requestAnimationFrame(() => {
450
+ setFlashingType(type)
451
+ if (info) {
452
+ setHighlightInfo(info)
453
+ }
454
+ setTimeout(() => {
455
+ setFlashingType(null)
456
+ setHighlightInfo(null)
457
+ }, 1200)
458
+ })
459
+ })
460
+ }, [])
461
+
462
+ const handleWordClick = useCallback((info: WordClickInfo) => {
463
+ if (debugLog) {
464
+ console.log('LyricsAnalyzer handleWordClick:', { info });
465
+ }
466
+
467
+ if (effectiveMode === 'delete_word') {
468
+ // Use the shared deleteWord utility function
469
+ const newData = deleteWord(data, info.word_id);
470
+ updateDataWithHistory(newData, 'delete word'); // Update history
471
+
472
+ // Flash to indicate the word was deleted
473
+ handleFlash('word');
474
+ return;
475
+ }
476
+
477
+ if (effectiveMode === 'highlight') {
478
+ // Find if this word is part of a correction
479
+ const correction = data.corrections?.find(c =>
480
+ c.corrected_word_id === info.word_id ||
481
+ c.word_id === info.word_id
482
+ );
483
+
484
+ if (correction) {
485
+ // For agentic corrections, show the detail card instead of just highlighting
486
+ if (correction.handler === 'AgenticCorrector') {
487
+ handleShowCorrectionDetail(info.word_id)
488
+ return
489
+ }
490
+
491
+ setHighlightInfo({
492
+ type: 'correction',
493
+ transcribed_words: [], // Required by type but not used for corrections
494
+ correction: correction
495
+ });
496
+ setFlashingType('word');
497
+ return;
498
+ }
499
+
500
+ // Find if this word is part of an anchor sequence
501
+ const anchor = data.anchor_sequences?.find(a =>
502
+ a.transcribed_word_ids.includes(info.word_id) ||
503
+ Object.values(a.reference_word_ids).some(ids =>
504
+ ids.includes(info.word_id)
505
+ )
506
+ );
507
+
508
+ if (anchor) {
509
+ // Create a temporary segment containing all words
510
+ const allWords = data.corrected_segments.flatMap(s => s.words)
511
+ const tempSegment: LyricsSegment = {
512
+ id: 'temp',
513
+ words: allWords,
514
+ text: allWords.map(w => w.text).join(' '),
515
+ start_time: allWords[0]?.start_time ?? null,
516
+ end_time: allWords[allWords.length - 1]?.end_time ?? null
517
+ }
518
+
519
+ const transcribedWords = getWordsFromIds(
520
+ [tempSegment],
521
+ anchor.transcribed_word_ids
522
+ );
523
+
524
+ const referenceWords = Object.fromEntries(
525
+ Object.entries(anchor.reference_word_ids).map(([source, ids]) => {
526
+ const sourceWords = data.reference_lyrics[source].segments.flatMap(s => s.words)
527
+ const tempSourceSegment: LyricsSegment = {
528
+ id: `temp-${source}`,
529
+ words: sourceWords,
530
+ text: sourceWords.map(w => w.text).join(' '),
531
+ start_time: sourceWords[0]?.start_time ?? null,
532
+ end_time: sourceWords[sourceWords.length - 1]?.end_time ?? null
533
+ }
534
+ return [source, getWordsFromIds([tempSourceSegment], ids)]
535
+ })
536
+ );
537
+
538
+ setHighlightInfo({
539
+ type: 'anchor',
540
+ sequence: anchor,
541
+ transcribed_words: transcribedWords,
542
+ reference_words: referenceWords
543
+ });
544
+ setFlashingType('word');
545
+ return;
546
+ }
547
+
548
+ // Find if this word is part of a gap sequence
549
+ const gap = data.gap_sequences?.find(g =>
550
+ g.transcribed_word_ids.includes(info.word_id)
551
+ );
552
+
553
+ if (gap) {
554
+ // Create a temporary segment containing all words
555
+ const allWords = data.corrected_segments.flatMap(s => s.words)
556
+ const tempSegment: LyricsSegment = {
557
+ id: 'temp',
558
+ words: allWords,
559
+ text: allWords.map(w => w.text).join(' '),
560
+ start_time: allWords[0]?.start_time ?? null,
561
+ end_time: allWords[allWords.length - 1]?.end_time ?? null
562
+ }
563
+
564
+ const transcribedWords = getWordsFromIds(
565
+ [tempSegment],
566
+ gap.transcribed_word_ids
567
+ );
568
+
569
+ const referenceWords = Object.fromEntries(
570
+ Object.entries(gap.reference_word_ids).map(([source, ids]) => {
571
+ const sourceWords = data.reference_lyrics[source].segments.flatMap(s => s.words)
572
+ const tempSourceSegment: LyricsSegment = {
573
+ id: `temp-${source}`,
574
+ words: sourceWords,
575
+ text: sourceWords.map(w => w.text).join(' '),
576
+ start_time: sourceWords[0]?.start_time ?? null,
577
+ end_time: sourceWords[sourceWords.length - 1]?.end_time ?? null
578
+ }
579
+ return [source, getWordsFromIds([tempSourceSegment], ids)]
580
+ })
581
+ );
582
+
583
+ setHighlightInfo({
584
+ type: 'gap',
585
+ sequence: gap,
586
+ transcribed_words: transcribedWords,
587
+ reference_words: referenceWords
588
+ });
589
+ setFlashingType('word');
590
+ return;
591
+ }
592
+ } else if (effectiveMode === 'edit') {
593
+ // Find the segment containing this word
594
+ const segmentIndex = data.corrected_segments.findIndex(segment =>
595
+ segment.words.some(word => word.id === info.word_id)
596
+ );
597
+
598
+ if (segmentIndex !== -1) {
599
+ const segment = data.corrected_segments[segmentIndex];
600
+ setEditModalSegment({
601
+ segment,
602
+ index: segmentIndex,
603
+ originalSegment: JSON.parse(JSON.stringify(segment))
604
+ });
605
+ }
606
+ }
607
+ }, [data, effectiveMode, setModalContent, handleFlash, deleteWord, updateDataWithHistory]);
608
+
609
+ // Annotation handlers (declared early for use in other callbacks)
610
+ const handleSaveAnnotation = useCallback((annotation: Omit<CorrectionAnnotation, 'annotation_id' | 'timestamp'>) => {
611
+ setAnnotations(prev => [...prev, annotation])
612
+ console.log('Annotation saved:', annotation)
613
+ }, [])
614
+
615
+ const handleSkipAnnotation = useCallback(() => {
616
+ console.log('Annotation skipped')
617
+ }, [])
618
+
619
+ const triggerAnnotationModal = useCallback((
620
+ originalText: string,
621
+ correctedText: string,
622
+ wordIdsAffected: string[],
623
+ gapId?: string
624
+ ) => {
625
+ if (!annotationsEnabled || isReadOnly) {
626
+ return
627
+ }
628
+
629
+ setPendingAnnotation({
630
+ originalText,
631
+ correctedText,
632
+ wordIdsAffected,
633
+ gapId
634
+ })
635
+ setIsAnnotationModalOpen(true)
636
+ }, [annotationsEnabled, isReadOnly])
637
+
638
+ const handleUpdateSegment = useCallback((updatedSegment: LyricsSegment) => {
639
+ if (!editModalSegment) return
640
+
641
+ if (debugLog) {
642
+ console.log('[DEBUG] handleUpdateSegment: Updating history from modal save', {
643
+ segmentIndex: editModalSegment.index,
644
+ currentHistoryIndex: historyIndex,
645
+ currentHistoryLength: history.length,
646
+ currentSegmentText: history[historyIndex]?.corrected_segments[editModalSegment.index]?.text,
647
+ updatedSegmentText: updatedSegment.text
648
+ });
649
+ }
650
+
651
+ // --- Ensure Immutability Here ---
652
+ const currentData = history[historyIndex];
653
+ const newSegments = currentData.corrected_segments.map((segment, i) =>
654
+ i === editModalSegment.index ? updatedSegment : segment
655
+ );
656
+ const newDataImmutable: CorrectionData = {
657
+ ...currentData,
658
+ corrected_segments: newSegments,
659
+ };
660
+ // --- End Immutability Ensure ---
661
+
662
+ updateDataWithHistory(newDataImmutable, 'update segment');
663
+
664
+ if (debugLog) {
665
+ console.log('[DEBUG] handleUpdateSegment: History updated (async)', {
666
+ newHistoryIndex: historyIndex + 1,
667
+ newHistoryLength: history.length - historyIndex === 1 ? history.length + 1 : historyIndex + 2
668
+ });
669
+ }
670
+
671
+ // Trigger annotation modal if enabled and text changed
672
+ const originalSegment = editModalSegment.originalSegment || editModalSegment.segment
673
+ if (originalSegment && originalSegment.text !== updatedSegment.text) {
674
+ // Get word IDs that were affected
675
+ const wordIds = updatedSegment.words.map(w => w.id)
676
+ triggerAnnotationModal(
677
+ originalSegment.text,
678
+ updatedSegment.text,
679
+ wordIds
680
+ )
681
+ }
682
+
683
+ setEditModalSegment(null)
684
+ }, [history, historyIndex, editModalSegment, updateDataWithHistory, triggerAnnotationModal])
685
+
686
+ const handleDeleteSegment = useCallback((segmentIndex: number) => {
687
+ const newData = deleteSegment(data, segmentIndex)
688
+ updateDataWithHistory(newData, 'delete segment')
689
+ }, [data, updateDataWithHistory])
690
+
691
+ // Correction action handlers
692
+ const handleRevertCorrection = useCallback((wordId: string) => {
693
+ // Find the correction for this word
694
+ const correction = data.corrections?.find(c =>
695
+ c.corrected_word_id === wordId || c.word_id === wordId
696
+ )
697
+
698
+ if (!correction) {
699
+ console.error('Correction not found for word:', wordId)
700
+ return
701
+ }
702
+
703
+ // Find the segment containing the corrected word
704
+ const segmentIndex = data.corrected_segments.findIndex(segment =>
705
+ segment.words.some(w => w.id === wordId)
706
+ )
707
+
708
+ if (segmentIndex === -1) {
709
+ console.error('Segment not found for word:', wordId)
710
+ return
711
+ }
712
+
713
+ const segment = data.corrected_segments[segmentIndex]
714
+
715
+ // Replace the corrected word with the original
716
+ const newWords = segment.words.map(word => {
717
+ if (word.id === wordId) {
718
+ return {
719
+ ...word,
720
+ text: correction.original_word,
721
+ id: correction.word_id // Restore original word ID
722
+ }
723
+ }
724
+ return word
725
+ })
726
+
727
+ // Rebuild segment text
728
+ const newText = newWords.map(w => w.text).join(' ')
729
+
730
+ const newSegment = {
731
+ ...segment,
732
+ words: newWords,
733
+ text: newText
734
+ }
735
+
736
+ // Update data
737
+ const newSegments = data.corrected_segments.map((seg, idx) =>
738
+ idx === segmentIndex ? newSegment : seg
739
+ )
740
+
741
+ // Remove the correction from the corrections list
742
+ const newCorrections = data.corrections?.filter(c =>
743
+ c.corrected_word_id !== wordId && c.word_id !== wordId
744
+ )
745
+
746
+ const newData: CorrectionData = {
747
+ ...data,
748
+ corrected_segments: newSegments,
749
+ corrections: newCorrections || []
750
+ }
751
+
752
+ updateDataWithHistory(newData, 'revert correction')
753
+
754
+ console.log('Reverted correction:', {
755
+ originalWord: correction.original_word,
756
+ correctedWord: correction.corrected_word,
757
+ wordId
758
+ })
759
+ }, [data, updateDataWithHistory])
760
+
761
+ const handleEditCorrection = useCallback((wordId: string) => {
762
+ // Find the segment containing this word
763
+ const segmentIndex = data.corrected_segments.findIndex(segment =>
764
+ segment.words.some(w => w.id === wordId)
765
+ )
766
+
767
+ if (segmentIndex === -1) {
768
+ console.error('Segment not found for word:', wordId)
769
+ return
770
+ }
771
+
772
+ // Open edit modal for this segment
773
+ const segment = data.corrected_segments[segmentIndex]
774
+ setEditModalSegment({
775
+ segment: segment,
776
+ index: segmentIndex,
777
+ originalSegment: segment
778
+ })
779
+ }, [data])
780
+
781
+ const handleAcceptCorrection = useCallback((wordId: string) => {
782
+ // For now, just log acceptance
783
+ // In the future, this could be tracked in the annotation system
784
+ console.log('Accepted correction for word:', wordId)
785
+
786
+ // TODO: Track acceptance in annotation system
787
+ // This could be used to build confidence in the AI's corrections over time
788
+ }, [])
789
+
790
+ const handleShowCorrectionDetail = useCallback((wordId: string) => {
791
+ // Find the correction for this word
792
+ const correction = data.corrections?.find(c =>
793
+ c.corrected_word_id === wordId || c.word_id === wordId
794
+ )
795
+
796
+ if (!correction) {
797
+ console.error('Correction not found for word:', wordId)
798
+ return
799
+ }
800
+
801
+ // Extract category from reason (format: "reason [CATEGORY] (confidence: XX%)")
802
+ const categoryMatch = correction.reason?.match(/\[([A-Z_]+)\]/)
803
+ const category = categoryMatch ? categoryMatch[1] : null
804
+
805
+ // Find the corrected word text
806
+ const correctedWord = data.corrected_segments
807
+ .flatMap(s => s.words)
808
+ .find(w => w.id === wordId)?.text || correction.corrected_word
809
+
810
+ setSelectedCorrection({
811
+ wordId,
812
+ originalWord: correction.original_word,
813
+ correctedWord: correctedWord,
814
+ category,
815
+ confidence: correction.confidence,
816
+ reason: correction.reason,
817
+ handler: correction.handler,
818
+ source: correction.source
819
+ })
820
+ setCorrectionDetailOpen(true)
821
+ }, [data])
822
+
823
+ const handleFinishReview = useCallback(() => {
824
+ console.log(`[TIMING] handleFinishReview - Current timing offset: ${timingOffsetMs}ms`);
825
+ setIsReviewModalOpen(true)
826
+ }, [timingOffsetMs])
827
+
828
+ const handleSubmitToServer = useCallback(async () => {
829
+ if (!apiClient) return
830
+
831
+ try {
832
+ if (debugLog) {
833
+ console.log('Submitting changes to server')
834
+ }
835
+
836
+ // Debug logging for timing offset
837
+ console.log(`[TIMING] handleSubmitToServer - Current timing offset: ${timingOffsetMs}ms`);
838
+
839
+ // Apply timing offset to the data before submission if needed
840
+ const dataToSubmit = timingOffsetMs !== 0
841
+ ? applyOffsetToCorrectionData(data, timingOffsetMs)
842
+ : data
843
+
844
+ // Log some example timestamps after potential offset application
845
+ if (dataToSubmit.corrected_segments.length > 0) {
846
+ const firstSegment = dataToSubmit.corrected_segments[0];
847
+ console.log(`[TIMING] Submitting data - First segment id: ${firstSegment.id}`);
848
+ console.log(`[TIMING] - start_time: ${firstSegment.start_time}, end_time: ${firstSegment.end_time}`);
849
+
850
+ if (firstSegment.words.length > 0) {
851
+ const firstWord = firstSegment.words[0];
852
+ console.log(`[TIMING] - first word "${firstWord.text}" time: ${firstWord.start_time} -> ${firstWord.end_time}`);
853
+ }
854
+ }
855
+
856
+ await apiClient.submitCorrections(dataToSubmit)
857
+
858
+ // Submit annotations if any were collected
859
+ if (annotations.length > 0) {
860
+ console.log(`Submitting ${annotations.length} annotations...`)
861
+ try {
862
+ await apiClient.submitAnnotations(annotations)
863
+ console.log('Annotations submitted successfully')
864
+ } catch (error) {
865
+ console.error('Failed to submit annotations:', error)
866
+ // Don't block the main submission if annotations fail
867
+ }
868
+ }
869
+
870
+ setIsReviewComplete(true)
871
+ setIsReviewModalOpen(false)
872
+
873
+ // Close the browser tab
874
+ window.close()
875
+ } catch (error) {
876
+ console.error('Failed to submit corrections:', error)
877
+ alert('Failed to submit corrections. Please try again.')
878
+ }
879
+ }, [apiClient, data, timingOffsetMs, annotations])
880
+
881
+ // Update play segment handler
882
+ const handlePlaySegment = useCallback((startTime: number) => {
883
+ if (window.seekAndPlayAudio) {
884
+ // Apply the timing offset to the start time
885
+ const adjustedStartTime = timingOffsetMs !== 0
886
+ ? startTime + (timingOffsetMs / 1000)
887
+ : startTime;
888
+
889
+ window.seekAndPlayAudio(adjustedStartTime)
890
+ }
891
+ }, [timingOffsetMs])
892
+
893
+ const handleResetCorrections = useCallback(() => {
894
+ if (window.confirm('Are you sure you want to reset all corrections? This cannot be undone.')) {
895
+ clearSavedData(initialData)
896
+ // Reset history to the original initial data
897
+ setHistory([JSON.parse(JSON.stringify(initialData))])
898
+ setHistoryIndex(0)
899
+ setModalContent(null)
900
+ setFlashingType(null)
901
+ setHighlightInfo(null)
902
+ setInteractionMode('edit')
903
+ }
904
+ }, [initialData])
905
+
906
+ const handleAddSegment = useCallback((beforeIndex: number) => {
907
+ const newData = addSegmentBefore(data, beforeIndex)
908
+ updateDataWithHistory(newData, 'add segment')
909
+ }, [data, updateDataWithHistory])
910
+
911
+ const handleSplitSegment = useCallback((segmentIndex: number, afterWordIndex: number) => {
912
+ const newData = splitSegment(data, segmentIndex, afterWordIndex)
913
+ if (newData) {
914
+ updateDataWithHistory(newData, 'split segment')
915
+ setEditModalSegment(null)
916
+ }
917
+ }, [data, updateDataWithHistory])
918
+
919
+ const handleMergeSegment = useCallback((segmentIndex: number, mergeWithNext: boolean) => {
920
+ const newData = mergeSegment(data, segmentIndex, mergeWithNext)
921
+ updateDataWithHistory(newData, 'merge segment')
922
+ setEditModalSegment(null)
923
+ }, [data, updateDataWithHistory])
924
+
925
+ const handleHandlerToggle = useCallback(async (handler: string, enabled: boolean) => {
926
+ if (!apiClient) return
927
+
928
+ try {
929
+ setIsUpdatingHandlers(true);
930
+
931
+ // Get current enabled handlers
932
+ const currentEnabled = new Set(data.metadata.enabled_handlers || [])
933
+
934
+ // Update the set based on the toggle
935
+ if (enabled) {
936
+ currentEnabled.add(handler)
937
+ } else {
938
+ currentEnabled.delete(handler)
939
+ }
940
+
941
+ // Call API to update handlers
942
+ const newData = await apiClient.updateHandlers(Array.from(currentEnabled))
943
+
944
+ // Update local state with new correction data
945
+ // This API call returns the *entire* new state, so treat it as a single history step
946
+ updateDataWithHistory(newData, `toggle handler ${handler}`); // Update history
947
+
948
+ // Clear any existing modals or highlights
949
+ setModalContent(null)
950
+ setFlashingType(null)
951
+ setHighlightInfo(null)
952
+
953
+ // Flash the updated corrections
954
+ handleFlash('corrected')
955
+ } catch (error) {
956
+ console.error('Failed to update handlers:', error)
957
+ alert('Failed to update correction handlers. Please try again.')
958
+ } finally {
959
+ setIsUpdatingHandlers(false);
960
+ }
961
+ }, [apiClient, data.metadata.enabled_handlers, handleFlash, updateDataWithHistory])
962
+
963
+ const handleHandlerClick = useCallback((handler: string) => {
964
+ if (debugLog) {
965
+ console.log('Handler clicked:', handler);
966
+ }
967
+ setFlashingHandler(handler);
968
+ setFlashingType('handler');
969
+ if (debugLog) {
970
+ console.log('Set flashingHandler to:', handler);
971
+ console.log('Set flashingType to: handler');
972
+ }
973
+ // Clear the flash after a short delay
974
+ setTimeout(() => {
975
+ if (debugLog) {
976
+ console.log('Clearing flash state');
977
+ }
978
+ setFlashingHandler(null);
979
+ setFlashingType(null);
980
+ }, 1500);
981
+ }, []);
982
+
983
+ // Wrap setModalSpacebarHandler in useCallback
984
+ const handleSetModalSpacebarHandler = useCallback((handler: (() => (e: KeyboardEvent) => void) | undefined) => {
985
+ if (debugLog) {
986
+ console.log('LyricsAnalyzer - Setting modal handler:', {
987
+ hasHandler: !!handler
988
+ })
989
+ }
990
+ // Update the global modal handler
991
+ setModalHandler(handler ? handler() : undefined, !!handler)
992
+ }, [])
993
+
994
+ // Add new handler for adding lyrics
995
+ const handleAddLyrics = useCallback(async (source: string, lyrics: string) => {
996
+ if (!apiClient) return
997
+
998
+ try {
999
+ setIsAddingLyrics(true)
1000
+ const newData = await apiClient.addLyrics(source, lyrics)
1001
+ // This API call returns the *entire* new state
1002
+ updateDataWithHistory(newData, 'add lyrics'); // Update history
1003
+ } finally {
1004
+ setIsAddingLyrics(false)
1005
+ }
1006
+ }, [apiClient, updateDataWithHistory])
1007
+
1008
+ const handleFindReplace = (findText: string, replaceText: string, options: { caseSensitive: boolean, useRegex: boolean, fullTextMode: boolean }) => {
1009
+ const newData = findAndReplace(data, findText, replaceText, options)
1010
+ updateDataWithHistory(newData, 'find/replace'); // Update history
1011
+ }
1012
+
1013
+ // Add handler for Un-Correct All functionality
1014
+ const handleUnCorrectAll = useCallback(() => {
1015
+ if (!originalData.original_segments) {
1016
+ console.warn('No original segments available for un-correcting')
1017
+ return
1018
+ }
1019
+
1020
+ if (window.confirm('Are you sure you want to revert all segments to their original transcribed state? This will undo all corrections made.')) {
1021
+ console.log('Un-Correct All: Reverting all segments to original transcribed state', {
1022
+ originalSegmentCount: originalData.original_segments.length,
1023
+ currentSegmentCount: data.corrected_segments.length
1024
+ })
1025
+
1026
+ // Create new data with original segments as corrected segments
1027
+ const newData: CorrectionData = {
1028
+ ...data,
1029
+ corrected_segments: JSON.parse(JSON.stringify(originalData.original_segments))
1030
+ }
1031
+
1032
+ updateDataWithHistory(newData, 'un-correct all segments')
1033
+ }
1034
+ }, [originalData.original_segments, data, updateDataWithHistory])
1035
+
1036
+ // Add handler for Replace All Lyrics functionality
1037
+ const handleReplaceAllLyrics = useCallback(() => {
1038
+ console.log('ReplaceAllLyrics - Opening modal')
1039
+ setIsReplaceAllLyricsModalOpen(true)
1040
+ }, [])
1041
+
1042
+ // Handle saving new segments from Replace All Lyrics
1043
+ const handleSaveReplaceAllLyrics = useCallback((newSegments: LyricsSegment[]) => {
1044
+ console.log('ReplaceAllLyrics - Saving new segments:', {
1045
+ segmentCount: newSegments.length,
1046
+ totalWords: newSegments.reduce((count, segment) => count + segment.words.length, 0)
1047
+ })
1048
+
1049
+ // Create new data with the replaced segments
1050
+ const newData = {
1051
+ ...data,
1052
+ corrected_segments: newSegments
1053
+ }
1054
+
1055
+ updateDataWithHistory(newData, 'replace all lyrics')
1056
+ setIsReplaceAllLyricsModalOpen(false)
1057
+ }, [data, updateDataWithHistory])
1058
+
1059
+
1060
+
1061
+ // Undo/Redo handlers
1062
+ const handleUndo = useCallback(() => {
1063
+ if (historyIndex > 0) {
1064
+ const newIndex = historyIndex - 1;
1065
+ if (debugLog) {
1066
+ console.log(`[DEBUG] Undo: moving from index ${historyIndex} to ${newIndex}. History length: ${history.length}`);
1067
+ }
1068
+ setHistoryIndex(newIndex);
1069
+ } else {
1070
+ if (debugLog) {
1071
+ console.log(`[DEBUG] Undo: already at the beginning (index ${historyIndex})`);
1072
+ }
1073
+ }
1074
+ }, [historyIndex, history])
1075
+
1076
+ const handleRedo = useCallback(() => {
1077
+ if (historyIndex < history.length - 1) {
1078
+ const newIndex = historyIndex + 1;
1079
+ if (debugLog) {
1080
+ console.log(`[DEBUG] Redo: moving from index ${historyIndex} to ${newIndex}. History length: ${history.length}`);
1081
+ }
1082
+ setHistoryIndex(newIndex);
1083
+ } else {
1084
+ if (debugLog) {
1085
+ console.log(`[DEBUG] Redo: already at the end (index ${historyIndex}, history length ${history.length})`);
1086
+ }
1087
+ }
1088
+ }, [historyIndex, history])
1089
+
1090
+ // Determine if Undo/Redo is possible
1091
+ const canUndo = historyIndex > 0
1092
+ const canRedo = historyIndex < history.length - 1
1093
+
1094
+ // Memoize the metric click handlers
1095
+ const metricClickHandlers = useMemo(() => ({
1096
+ anchor: () => handleFlash('anchor'),
1097
+ corrected: () => handleFlash('corrected'),
1098
+ uncorrected: () => handleFlash('uncorrected')
1099
+ }), [handleFlash]);
1100
+
1101
+ // Determine if any modal is open to disable highlighting
1102
+ const isAnyModalOpenMemo = useMemo(() => isAnyModalOpen, [isAnyModalOpen]);
1103
+
1104
+ // For the TranscriptionView, we need to apply the timing offset when displaying
1105
+ const displayData = useMemo(() => {
1106
+ return timingOffsetMs !== 0
1107
+ ? applyOffsetToCorrectionData(data, timingOffsetMs)
1108
+ : data;
1109
+ }, [data, timingOffsetMs]);
1110
+
1111
+ // Handler for opening the timing offset modal
1112
+ const handleOpenTimingOffsetModal = useCallback(() => {
1113
+ setIsTimingOffsetModalOpen(true)
1114
+ }, [])
1115
+
1116
+ // Handler for applying the timing offset
1117
+ const handleApplyTimingOffset = useCallback((offsetMs: number) => {
1118
+ // Only update if the offset has changed
1119
+ if (offsetMs !== timingOffsetMs) {
1120
+ console.log(`[TIMING] handleApplyTimingOffset: Changing offset from ${timingOffsetMs}ms to ${offsetMs}ms`);
1121
+ setTimingOffsetMs(offsetMs)
1122
+
1123
+ // If we're applying an offset, we don't need to update history
1124
+ // since we're not modifying the original data
1125
+ if (debugLog) {
1126
+ console.log(`[DEBUG] handleApplyTimingOffset: Setting offset to ${offsetMs}ms`);
1127
+ }
1128
+ } else {
1129
+ console.log(`[TIMING] handleApplyTimingOffset: Offset unchanged at ${offsetMs}ms`);
1130
+ }
1131
+ }, [timingOffsetMs])
1132
+
1133
+ // Add logging for timing offset changes
1134
+ useEffect(() => {
1135
+ console.log(`[TIMING] timingOffsetMs changed to: ${timingOffsetMs}ms`);
1136
+ }, [timingOffsetMs]);
1137
+
1138
+ return (
1139
+ <Box sx={{
1140
+ p: 1,
1141
+ pb: 3,
1142
+ maxWidth: '100%',
1143
+ overflowX: 'hidden'
1144
+ }}>
1145
+ <MemoizedHeader
1146
+ isReadOnly={isReadOnly}
1147
+ onFileLoad={onFileLoad}
1148
+ data={data}
1149
+ onMetricClick={metricClickHandlers}
1150
+ effectiveMode={effectiveMode}
1151
+ onModeChange={setInteractionMode}
1152
+ apiClient={apiClient}
1153
+ audioHash={audioHash}
1154
+ onTimeUpdate={setCurrentAudioTime}
1155
+ onHandlerToggle={handleHandlerToggle}
1156
+ isUpdatingHandlers={isUpdatingHandlers}
1157
+ onHandlerClick={handleHandlerClick}
1158
+ onFindReplace={() => setIsFindReplaceModalOpen(true)}
1159
+ onEditAll={handleReplaceAllLyrics}
1160
+ onTimingOffset={handleOpenTimingOffsetModal}
1161
+ timingOffsetMs={timingOffsetMs}
1162
+ onUndo={handleUndo}
1163
+ onRedo={handleRedo}
1164
+ canUndo={canUndo}
1165
+ canRedo={canRedo}
1166
+ onUnCorrectAll={handleUnCorrectAll}
1167
+ />
1168
+
1169
+ <Grid container direction={isMobile ? 'column' : 'row'}>
1170
+ <Grid item xs={12} md={6}>
1171
+ <MemoizedTranscriptionView
1172
+ data={displayData}
1173
+ mode={effectiveMode}
1174
+ onElementClick={setModalContent}
1175
+ onWordClick={handleWordClick}
1176
+ flashingType={flashingType}
1177
+ flashingHandler={flashingHandler}
1178
+ highlightInfo={highlightInfo}
1179
+ onPlaySegment={handlePlaySegment}
1180
+ currentTime={currentAudioTime}
1181
+ anchors={data.anchor_sequences}
1182
+ disableHighlighting={isAnyModalOpenMemo}
1183
+ onDataChange={(updatedData) => {
1184
+ // Direct data change from TranscriptionView (e.g., drag-and-drop)
1185
+ // needs to update history
1186
+ updateDataWithHistory(updatedData, 'direct data change');
1187
+ }}
1188
+ />
1189
+ {!isReadOnly && apiClient && (
1190
+ <Box sx={{
1191
+ mt: 2,
1192
+ mb: 3,
1193
+ display: 'flex',
1194
+ justifyContent: 'space-between',
1195
+ width: '100%'
1196
+ }}>
1197
+ <Button
1198
+ variant="outlined"
1199
+ color="warning"
1200
+ onClick={handleResetCorrections}
1201
+ startIcon={<RestoreFromTrash />}
1202
+ >
1203
+ Reset Corrections
1204
+ </Button>
1205
+ <Button
1206
+ variant="contained"
1207
+ onClick={handleFinishReview}
1208
+ disabled={isReviewComplete}
1209
+ endIcon={<OndemandVideo />}
1210
+ >
1211
+ {isReviewComplete ? 'Review Complete' : 'Preview Video'}
1212
+ </Button>
1213
+ </Box>
1214
+ )}
1215
+ </Grid>
1216
+ <Grid item xs={12} md={6}>
1217
+ <MemoizedReferenceView
1218
+ referenceSources={data.reference_lyrics}
1219
+ anchors={data.anchor_sequences}
1220
+ gaps={data.gap_sequences}
1221
+ mode={effectiveMode}
1222
+ onElementClick={setModalContent}
1223
+ onWordClick={handleWordClick}
1224
+ flashingType={flashingType}
1225
+ highlightInfo={highlightInfo}
1226
+ currentSource={currentSource}
1227
+ onSourceChange={setCurrentSource}
1228
+ corrected_segments={data.corrected_segments}
1229
+ corrections={data.corrections}
1230
+ onAddLyrics={() => setIsAddLyricsModalOpen(true)}
1231
+ />
1232
+ </Grid>
1233
+ </Grid>
1234
+
1235
+
1236
+
1237
+ <EditModal
1238
+ open={Boolean(editModalSegment)}
1239
+ onClose={() => {
1240
+ setEditModalSegment(null)
1241
+ handleSetModalSpacebarHandler(undefined)
1242
+ }}
1243
+ segment={editModalSegment?.segment ?
1244
+ timingOffsetMs !== 0 ?
1245
+ applyOffsetToSegment(editModalSegment.segment, timingOffsetMs) :
1246
+ editModalSegment.segment :
1247
+ null}
1248
+ segmentIndex={editModalSegment?.index ?? null}
1249
+ originalSegment={editModalSegment?.originalSegment ?
1250
+ timingOffsetMs !== 0 ?
1251
+ applyOffsetToSegment(editModalSegment.originalSegment, timingOffsetMs) :
1252
+ editModalSegment.originalSegment :
1253
+ null}
1254
+ onSave={handleUpdateSegment}
1255
+ onDelete={handleDeleteSegment}
1256
+ onAddSegment={handleAddSegment}
1257
+ onSplitSegment={handleSplitSegment}
1258
+ onMergeSegment={handleMergeSegment}
1259
+ onPlaySegment={handlePlaySegment}
1260
+ currentTime={currentAudioTime}
1261
+ setModalSpacebarHandler={handleSetModalSpacebarHandler}
1262
+ originalTranscribedSegment={
1263
+ editModalSegment?.segment && editModalSegment?.index !== null && originalData.original_segments
1264
+ ? (() => {
1265
+ const origSegment = originalData.original_segments.find(
1266
+ (s: LyricsSegment) => s.id === editModalSegment.segment.id
1267
+ ) || null;
1268
+
1269
+ return origSegment && timingOffsetMs !== 0
1270
+ ? applyOffsetToSegment(origSegment, timingOffsetMs)
1271
+ : origSegment;
1272
+ })()
1273
+ : null
1274
+ }
1275
+ />
1276
+
1277
+ <ReviewChangesModal
1278
+ open={isReviewModalOpen}
1279
+ onClose={() => setIsReviewModalOpen(false)}
1280
+ originalData={originalData}
1281
+ updatedData={data}
1282
+ onSubmit={handleSubmitToServer}
1283
+ apiClient={apiClient}
1284
+ setModalSpacebarHandler={handleSetModalSpacebarHandler}
1285
+ timingOffsetMs={timingOffsetMs}
1286
+ />
1287
+
1288
+ <AddLyricsModal
1289
+ open={isAddLyricsModalOpen}
1290
+ onClose={() => setIsAddLyricsModalOpen(false)}
1291
+ onSubmit={handleAddLyrics}
1292
+ isSubmitting={isAddingLyrics}
1293
+ />
1294
+
1295
+ <FindReplaceModal
1296
+ open={isFindReplaceModalOpen}
1297
+ onClose={() => setIsFindReplaceModalOpen(false)}
1298
+ onReplace={handleFindReplace}
1299
+ data={data}
1300
+ />
1301
+
1302
+ <TimingOffsetModal
1303
+ open={isTimingOffsetModalOpen}
1304
+ onClose={() => setIsTimingOffsetModalOpen(false)}
1305
+ currentOffset={timingOffsetMs}
1306
+ onApply={handleApplyTimingOffset}
1307
+ />
1308
+
1309
+ <ReplaceAllLyricsModal
1310
+ open={isReplaceAllLyricsModalOpen}
1311
+ onClose={() => setIsReplaceAllLyricsModalOpen(false)}
1312
+ onSave={handleSaveReplaceAllLyrics}
1313
+ onPlaySegment={handlePlaySegment}
1314
+ currentTime={currentAudioTime}
1315
+ setModalSpacebarHandler={handleSetModalSpacebarHandler}
1316
+ />
1317
+
1318
+ {pendingAnnotation && (
1319
+ <CorrectionAnnotationModal
1320
+ open={isAnnotationModalOpen}
1321
+ onClose={() => {
1322
+ setIsAnnotationModalOpen(false)
1323
+ setPendingAnnotation(null)
1324
+ }}
1325
+ onSave={handleSaveAnnotation}
1326
+ onSkip={handleSkipAnnotation}
1327
+ originalText={pendingAnnotation.originalText}
1328
+ correctedText={pendingAnnotation.correctedText}
1329
+ wordIdsAffected={pendingAnnotation.wordIdsAffected}
1330
+ agenticProposal={undefined} // TODO: Pass agentic proposal if available
1331
+ referenceSources={Object.keys(data.reference_lyrics || {})}
1332
+ audioHash={audioHash}
1333
+ artist={data.metadata?.audio_filepath?.split('/').pop()?.split('.')[0] || 'Unknown'}
1334
+ title={data.metadata?.audio_filepath?.split('/').pop()?.split('.')[0] || 'Unknown'}
1335
+ sessionId={audioHash} // Use audio hash as session ID for now
1336
+ gapId={pendingAnnotation.gapId}
1337
+ />
1338
+ )}
1339
+
1340
+ {selectedCorrection && (
1341
+ <CorrectionDetailCard
1342
+ open={correctionDetailOpen}
1343
+ onClose={() => {
1344
+ setCorrectionDetailOpen(false)
1345
+ setSelectedCorrection(null)
1346
+ }}
1347
+ originalWord={selectedCorrection.originalWord}
1348
+ correctedWord={selectedCorrection.correctedWord}
1349
+ category={selectedCorrection.category}
1350
+ confidence={selectedCorrection.confidence}
1351
+ reason={selectedCorrection.reason}
1352
+ handler={selectedCorrection.handler}
1353
+ source={selectedCorrection.source}
1354
+ onRevert={() => {
1355
+ handleRevertCorrection(selectedCorrection.wordId)
1356
+ setCorrectionDetailOpen(false)
1357
+ setSelectedCorrection(null)
1358
+ }}
1359
+ onEdit={() => {
1360
+ handleEditCorrection(selectedCorrection.wordId)
1361
+ setCorrectionDetailOpen(false)
1362
+ setSelectedCorrection(null)
1363
+ }}
1364
+ onAccept={() => {
1365
+ handleAcceptCorrection(selectedCorrection.wordId)
1366
+ setCorrectionDetailOpen(false)
1367
+ setSelectedCorrection(null)
1368
+ }}
1369
+ />
1370
+ )}
1371
+ </Box>
1372
+ )
1373
+ }