karaoke-gen 0.75.54__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.

Potentially problematic release.


This version of karaoke-gen might be problematic. Click here for more details.

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