karaoke-gen 0.57.0__py3-none-any.whl → 0.71.23__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 +1815 -0
  26. karaoke_gen/video_background_processor.py +351 -0
  27. karaoke_gen-0.71.23.dist-info/METADATA +610 -0
  28. karaoke_gen-0.71.23.dist-info/RECORD +275 -0
  29. {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.23.dist-info}/WHEEL +1 -1
  30. {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.23.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.23.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,702 @@
1
+ import {
2
+ Dialog,
3
+ DialogTitle,
4
+ DialogContent,
5
+ DialogActions,
6
+ IconButton,
7
+ Box,
8
+ CircularProgress,
9
+ Typography
10
+ } from '@mui/material'
11
+ import CloseIcon from '@mui/icons-material/Close'
12
+ import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline'
13
+ import StopIcon from '@mui/icons-material/Stop'
14
+ import { LyricsSegment, Word } from '../types'
15
+ import { useState, useEffect, useCallback, useMemo, memo } from 'react'
16
+ import { nanoid } from 'nanoid'
17
+ import useManualSync from '../hooks/useManualSync'
18
+ import EditTimelineSection from './EditTimelineSection'
19
+ import EditWordList from './EditWordList'
20
+ import EditActionBar from './EditActionBar'
21
+
22
+ // Extract TimelineSection into a separate memoized component
23
+ interface TimelineSectionProps {
24
+ words: Word[]
25
+ timeRange: { start: number, end: number }
26
+ originalSegment: LyricsSegment
27
+ editedSegment: LyricsSegment
28
+ currentTime: number
29
+ isManualSyncing: boolean
30
+ syncWordIndex: number
31
+ isSpacebarPressed: boolean
32
+ onWordUpdate: (index: number, updates: Partial<Word>) => void
33
+ onPlaySegment?: (startTime: number) => void
34
+ startManualSync: () => void
35
+ isGlobal: boolean
36
+ }
37
+
38
+ const MemoizedTimelineSection = memo(function TimelineSection({
39
+ words,
40
+ timeRange,
41
+ originalSegment,
42
+ editedSegment,
43
+ currentTime,
44
+ isManualSyncing,
45
+ syncWordIndex,
46
+ isSpacebarPressed,
47
+ onWordUpdate,
48
+ onPlaySegment,
49
+ startManualSync,
50
+ isGlobal
51
+ }: TimelineSectionProps) {
52
+ return (
53
+ <EditTimelineSection
54
+ words={words}
55
+ startTime={timeRange.start}
56
+ endTime={timeRange.end}
57
+ originalStartTime={originalSegment.start_time}
58
+ originalEndTime={originalSegment.end_time}
59
+ currentStartTime={editedSegment.start_time}
60
+ currentEndTime={editedSegment.end_time}
61
+ currentTime={currentTime}
62
+ isManualSyncing={isManualSyncing}
63
+ syncWordIndex={syncWordIndex}
64
+ isSpacebarPressed={isSpacebarPressed}
65
+ onWordUpdate={onWordUpdate}
66
+ onPlaySegment={onPlaySegment}
67
+ startManualSync={startManualSync}
68
+ isGlobal={isGlobal}
69
+ />
70
+ )
71
+ })
72
+
73
+ // Extract WordList into a separate memoized component
74
+ interface WordListProps {
75
+ words: Word[]
76
+ onWordUpdate: (index: number, updates: Partial<Word>) => void
77
+ onSplitWord: (index: number) => void
78
+ onMergeWords: (index: number) => void
79
+ onAddWord: (index?: number) => void
80
+ onRemoveWord: (index: number) => void
81
+ onSplitSegment?: (wordIndex: number) => void
82
+ onAddSegment?: (beforeIndex: number) => void
83
+ onMergeSegment?: (mergeWithNext: boolean) => void
84
+ isGlobal: boolean
85
+ }
86
+
87
+ const MemoizedWordList = memo(function WordList({
88
+ words,
89
+ onWordUpdate,
90
+ onSplitWord,
91
+ onMergeWords,
92
+ onAddWord,
93
+ onRemoveWord,
94
+ onSplitSegment,
95
+ onAddSegment,
96
+ onMergeSegment,
97
+ isGlobal
98
+ }: WordListProps) {
99
+ return (
100
+ <EditWordList
101
+ words={words}
102
+ onWordUpdate={onWordUpdate}
103
+ onSplitWord={onSplitWord}
104
+ onMergeWords={onMergeWords}
105
+ onAddWord={onAddWord}
106
+ onRemoveWord={onRemoveWord}
107
+ onSplitSegment={onSplitSegment}
108
+ onAddSegment={onAddSegment}
109
+ onMergeSegment={onMergeSegment}
110
+ isGlobal={isGlobal}
111
+ />
112
+ )
113
+ })
114
+
115
+ // Extract ActionBar into a separate memoized component
116
+ interface ActionBarProps {
117
+ onReset: () => void
118
+ onRevertToOriginal?: () => void
119
+ onDelete?: () => void
120
+ onClose: () => void
121
+ onSave: () => void
122
+ editedSegment: LyricsSegment | null
123
+ originalTranscribedSegment?: LyricsSegment | null
124
+ isGlobal: boolean
125
+ }
126
+
127
+ const MemoizedActionBar = memo(function ActionBar({
128
+ onReset,
129
+ onRevertToOriginal,
130
+ onDelete,
131
+ onClose,
132
+ onSave,
133
+ editedSegment,
134
+ originalTranscribedSegment,
135
+ isGlobal
136
+ }: ActionBarProps) {
137
+ return (
138
+ <EditActionBar
139
+ onReset={onReset}
140
+ onRevertToOriginal={onRevertToOriginal}
141
+ onDelete={onDelete}
142
+ onClose={onClose}
143
+ onSave={onSave}
144
+ editedSegment={editedSegment}
145
+ originalTranscribedSegment={originalTranscribedSegment}
146
+ isGlobal={isGlobal}
147
+ />
148
+ )
149
+ })
150
+
151
+ interface EditModalProps {
152
+ open: boolean
153
+ onClose: () => void
154
+ segment: LyricsSegment | null
155
+ segmentIndex: number | null
156
+ originalSegment: LyricsSegment | null
157
+ onSave: (updatedSegment: LyricsSegment) => void
158
+ onPlaySegment?: (startTime: number) => void
159
+ currentTime?: number
160
+ onDelete?: (segmentIndex: number) => void
161
+ onAddSegment?: (segmentIndex: number) => void
162
+ onSplitSegment?: (segmentIndex: number, afterWordIndex: number) => void
163
+ onMergeSegment?: (segmentIndex: number, mergeWithNext: boolean) => void
164
+ setModalSpacebarHandler: (handler: (() => (e: KeyboardEvent) => void) | undefined) => void
165
+ originalTranscribedSegment?: LyricsSegment | null
166
+ isGlobal?: boolean
167
+ isLoading?: boolean
168
+ }
169
+
170
+ export default function EditModal({
171
+ open,
172
+ onClose,
173
+ segment,
174
+ segmentIndex,
175
+ originalSegment,
176
+ onSave,
177
+ onPlaySegment,
178
+ currentTime = 0,
179
+ onDelete,
180
+ onAddSegment,
181
+ onSplitSegment,
182
+ onMergeSegment,
183
+ setModalSpacebarHandler,
184
+ originalTranscribedSegment,
185
+ isGlobal = false,
186
+ isLoading = false
187
+ }: EditModalProps) {
188
+ // console.log('EditModal - Render', {
189
+ // open,
190
+ // isGlobal,
191
+ // isLoading,
192
+ // hasSegment: !!segment,
193
+ // segmentIndex,
194
+ // hasOriginalSegment: !!originalSegment,
195
+ // hasOriginalTranscribedSegment: !!originalTranscribedSegment
196
+ // });
197
+
198
+ const [editedSegment, setEditedSegment] = useState<LyricsSegment | null>(segment)
199
+ const [isPlaying, setIsPlaying] = useState(false)
200
+
201
+ // Define updateSegment first since the hook depends on it
202
+ const updateSegment = useCallback((newWords: Word[]) => {
203
+ if (!editedSegment) return;
204
+
205
+ const validStartTimes = newWords.map(w => w.start_time).filter((t): t is number => t !== null)
206
+ const validEndTimes = newWords.map(w => w.end_time).filter((t): t is number => t !== null)
207
+
208
+ const segmentStartTime = validStartTimes.length > 0 ? Math.min(...validStartTimes) : null
209
+ const segmentEndTime = validEndTimes.length > 0 ? Math.max(...validEndTimes) : null
210
+
211
+ setEditedSegment({
212
+ ...editedSegment,
213
+ words: newWords,
214
+ text: newWords.map(w => w.text).join(' '),
215
+ start_time: segmentStartTime,
216
+ end_time: segmentEndTime
217
+ })
218
+ }, [editedSegment])
219
+
220
+ // Use the manual sync hook
221
+ const {
222
+ isManualSyncing,
223
+ syncWordIndex,
224
+ startManualSync,
225
+ cleanupManualSync,
226
+ handleSpacebar,
227
+ isSpacebarPressed
228
+ } = useManualSync({
229
+ editedSegment,
230
+ currentTime,
231
+ onPlaySegment,
232
+ updateSegment
233
+ })
234
+
235
+ const handleClose = useCallback(() => {
236
+ // console.log('EditModal - handleClose called');
237
+ cleanupManualSync()
238
+ onClose()
239
+ }, [onClose, cleanupManualSync])
240
+
241
+ // Update the spacebar handler when modal state changes
242
+ useEffect(() => {
243
+ const spacebarHandler = handleSpacebar // Capture the current handler
244
+
245
+ if (open) {
246
+ // console.log('EditModal - Setting up modal spacebar handler', {
247
+ // hasPlaySegment: !!onPlaySegment,
248
+ // editedSegmentId: editedSegment?.id,
249
+ // handlerFunction: spacebarHandler.toString().slice(0, 100),
250
+ // isLoading
251
+ // })
252
+
253
+ // Create a function that will be called by the global event listeners
254
+ const handleKeyEvent = (e: KeyboardEvent) => {
255
+ if (e.code === 'Space') {
256
+ spacebarHandler(e)
257
+ }
258
+ }
259
+
260
+ setModalSpacebarHandler(() => handleKeyEvent)
261
+
262
+ // Only cleanup when the effect is re-run or the modal is closed
263
+ return () => {
264
+ if (!open) {
265
+ // console.log('EditModal - Cleanup: clearing modal spacebar handler')
266
+ setModalSpacebarHandler(undefined)
267
+ }
268
+ }
269
+ }
270
+ }, [
271
+ open,
272
+ handleSpacebar,
273
+ setModalSpacebarHandler,
274
+ editedSegment?.id,
275
+ onPlaySegment,
276
+ isLoading
277
+ ])
278
+
279
+ // Update isPlaying when currentTime changes
280
+ useEffect(() => {
281
+ if (editedSegment) {
282
+ const startTime = editedSegment.start_time ?? 0
283
+ const endTime = editedSegment.end_time ?? 0
284
+ const isWithinSegment = currentTime >= startTime && currentTime <= endTime
285
+
286
+ setIsPlaying(isWithinSegment && window.isAudioPlaying === true)
287
+ }
288
+ }, [currentTime, editedSegment])
289
+
290
+ // All useEffect hooks
291
+ useEffect(() => {
292
+ // console.log('EditModal - segment changed', {
293
+ // hasSegment: !!segment,
294
+ // segmentId: segment?.id,
295
+ // wordCount: segment?.words.length
296
+ // });
297
+ setEditedSegment(segment)
298
+ }, [segment])
299
+
300
+ // Auto-stop sync if we go past the end time
301
+ useEffect(() => {
302
+ if (!editedSegment) return
303
+
304
+ const endTime = editedSegment.end_time ?? 0
305
+
306
+ if (window.isAudioPlaying && currentTime > endTime) {
307
+ // console.log('Stopping playback: current time exceeded end time')
308
+ window.toggleAudioPlayback?.()
309
+ cleanupManualSync()
310
+ }
311
+
312
+ }, [isManualSyncing, editedSegment, currentTime, cleanupManualSync])
313
+
314
+ // Add a function to get safe time values
315
+ const getSafeTimeRange = useCallback((segment: LyricsSegment | null) => {
316
+ if (!segment) return { start: 0, end: 1 }; // Default 1-second range
317
+ const start = segment.start_time ?? 0;
318
+ const end = segment.end_time ?? (start + 1);
319
+ return { start, end };
320
+ }, [])
321
+
322
+ // Define all handler functions with useCallback before the early return
323
+ const handleWordChange = useCallback((index: number, updates: Partial<Word>) => {
324
+ if (!editedSegment) return;
325
+ const newWords = [...editedSegment.words]
326
+ newWords[index] = {
327
+ ...newWords[index],
328
+ ...updates
329
+ }
330
+ updateSegment(newWords)
331
+ }, [editedSegment, updateSegment])
332
+
333
+ const handleAddWord = useCallback((index?: number) => {
334
+ if (!editedSegment) return;
335
+ const newWords = [...editedSegment.words]
336
+ let newWord: Word
337
+
338
+ if (index === undefined) {
339
+ // Add at end
340
+ const lastWord = newWords[newWords.length - 1]
341
+ const lastEndTime = lastWord.end_time ?? 0
342
+ newWord = {
343
+ id: nanoid(),
344
+ text: '',
345
+ start_time: lastEndTime,
346
+ end_time: lastEndTime + 0.5,
347
+ confidence: 1.0
348
+ }
349
+ newWords.push(newWord)
350
+ } else {
351
+ // Add between words
352
+ const prevWord = newWords[index]
353
+ const nextWord = newWords[index + 1]
354
+ const midTime = prevWord ?
355
+ (nextWord ?
356
+ ((prevWord.end_time ?? 0) + (nextWord.start_time ?? 0)) / 2 :
357
+ (prevWord.end_time ?? 0) + 0.5
358
+ ) :
359
+ (nextWord ? (nextWord.start_time ?? 0) - 0.5 : 0)
360
+
361
+ newWord = {
362
+ id: nanoid(),
363
+ text: '',
364
+ start_time: midTime - 0.25,
365
+ end_time: midTime + 0.25,
366
+ confidence: 1.0
367
+ }
368
+ newWords.splice(index + 1, 0, newWord)
369
+ }
370
+
371
+ updateSegment(newWords)
372
+ }, [editedSegment, updateSegment])
373
+
374
+ const handleSplitWord = useCallback((index: number) => {
375
+ if (!editedSegment) return;
376
+ const word = editedSegment.words[index]
377
+ const startTime = word.start_time ?? 0
378
+ const endTime = word.end_time ?? startTime + 0.5
379
+ const totalDuration = endTime - startTime
380
+
381
+ // Split on any number of spaces and filter out empty strings
382
+ const words = word.text.split(/\s+/).filter(w => w.length > 0)
383
+
384
+ if (words.length <= 1) {
385
+ // If no spaces found, split the word in half as before
386
+ const firstHalf = word.text.slice(0, Math.ceil(word.text.length / 2))
387
+ const secondHalf = word.text.slice(Math.ceil(word.text.length / 2))
388
+ words[0] = firstHalf
389
+ words[1] = secondHalf
390
+ }
391
+
392
+ // Calculate time per word
393
+ const timePerWord = totalDuration / words.length
394
+
395
+ // Create new word objects with evenly distributed times
396
+ const newWords = words.map((text, i) => ({
397
+ id: nanoid(),
398
+ text,
399
+ start_time: startTime + (i * timePerWord),
400
+ end_time: startTime + ((i + 1) * timePerWord),
401
+ confidence: 1.0
402
+ }))
403
+
404
+ // Replace the original word with the new words
405
+ const allWords = [...editedSegment.words]
406
+ allWords.splice(index, 1, ...newWords)
407
+
408
+ updateSegment(allWords)
409
+ }, [editedSegment, updateSegment])
410
+
411
+ const handleMergeWords = useCallback((index: number) => {
412
+ if (!editedSegment) return;
413
+ if (index >= editedSegment.words.length - 1) return
414
+
415
+ const word1 = editedSegment.words[index]
416
+ const word2 = editedSegment.words[index + 1]
417
+ const newWords = [...editedSegment.words]
418
+
419
+ newWords.splice(index, 2, {
420
+ id: nanoid(),
421
+ text: `${word1.text} ${word2.text}`.trim(),
422
+ start_time: word1.start_time ?? null,
423
+ end_time: word2.end_time ?? null,
424
+ confidence: 1.0
425
+ })
426
+
427
+ updateSegment(newWords)
428
+ }, [editedSegment, updateSegment])
429
+
430
+ const handleRemoveWord = useCallback((index: number) => {
431
+ if (!editedSegment) return;
432
+ const newWords = editedSegment.words.filter((_, i) => i !== index)
433
+ updateSegment(newWords)
434
+ }, [editedSegment, updateSegment])
435
+
436
+ const handleReset = useCallback(() => {
437
+ if (!originalSegment) return
438
+
439
+ console.log('EditModal - Resetting to original:', {
440
+ isGlobal,
441
+ originalSegmentId: originalSegment.id,
442
+ originalWordCount: originalSegment.words.length
443
+ })
444
+
445
+ setEditedSegment(JSON.parse(JSON.stringify(originalSegment)))
446
+ }, [originalSegment, isGlobal])
447
+
448
+ const handleRevertToOriginal = useCallback(() => {
449
+ if (!originalTranscribedSegment) return
450
+
451
+ console.log('EditModal - Reverting to original transcribed:', {
452
+ isGlobal,
453
+ originalTranscribedSegmentId: originalTranscribedSegment.id,
454
+ originalTranscribedWordCount: originalTranscribedSegment.words.length
455
+ })
456
+
457
+ setEditedSegment(JSON.parse(JSON.stringify(originalTranscribedSegment)))
458
+ }, [originalTranscribedSegment, isGlobal])
459
+
460
+ const handleSave = useCallback(() => {
461
+ if (!editedSegment || !segment) return;
462
+
463
+ console.log('EditModal - Saving segment:', {
464
+ isGlobal,
465
+ segmentIndex,
466
+ originalText: segment?.text,
467
+ editedText: editedSegment.text,
468
+ wordCount: editedSegment.words.length,
469
+ firstWord: editedSegment.words[0],
470
+ lastWord: editedSegment.words[editedSegment.words.length - 1],
471
+ timeRange: `${editedSegment.start_time?.toFixed(4) ?? 'N/A'} - ${editedSegment.end_time?.toFixed(4) ?? 'N/A'}`
472
+ })
473
+ onSave(editedSegment)
474
+ onClose()
475
+ }, [editedSegment, isGlobal, segmentIndex, segment, onSave, onClose])
476
+
477
+ const handleDelete = useCallback(() => {
478
+ if (segmentIndex !== null) {
479
+ onDelete?.(segmentIndex)
480
+ onClose()
481
+ }
482
+ }, [segmentIndex, onDelete, onClose])
483
+
484
+ const handleSplitSegment = useCallback((wordIndex: number) => {
485
+ if (segmentIndex !== null && editedSegment) {
486
+ handleSave() // Save current changes first
487
+ onSplitSegment?.(segmentIndex, wordIndex)
488
+ }
489
+ }, [segmentIndex, editedSegment, handleSave, onSplitSegment])
490
+
491
+ const handleMergeSegment = useCallback((mergeWithNext: boolean) => {
492
+ if (segmentIndex !== null && editedSegment) {
493
+ handleSave() // Save current changes first
494
+ onMergeSegment?.(segmentIndex, mergeWithNext)
495
+ onClose()
496
+ }
497
+ }, [segmentIndex, editedSegment, handleSave, onMergeSegment, onClose])
498
+
499
+ // Handle play/stop button click
500
+ const handlePlayButtonClick = useCallback(() => {
501
+ if (!segment?.start_time || !onPlaySegment) return
502
+
503
+ if (isPlaying) {
504
+ // Stop playback
505
+ if (window.toggleAudioPlayback) {
506
+ window.toggleAudioPlayback()
507
+ }
508
+ } else {
509
+ // Start playback
510
+ onPlaySegment(segment.start_time)
511
+ }
512
+ }, [segment?.start_time, onPlaySegment, isPlaying])
513
+
514
+ // Calculate timeRange before the early return
515
+ const timeRange = useMemo(() => {
516
+ if (!editedSegment) return { start: 0, end: 1 };
517
+ return getSafeTimeRange(editedSegment);
518
+ }, [getSafeTimeRange, editedSegment]);
519
+
520
+ // Memoize the dialog title to prevent re-renders
521
+ const dialogTitle = useMemo(() => {
522
+ // console.log('EditModal - Rendering dialog title', { isLoading, isGlobal });
523
+
524
+ if (isLoading) {
525
+ return (
526
+ <DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
527
+ <Box sx={{ flex: 1, display: 'flex', alignItems: 'center', gap: 1 }}>
528
+ Loading {isGlobal ? 'All Words' : `Segment ${segmentIndex}`}...
529
+ </Box>
530
+ <IconButton onClick={onClose} sx={{ ml: 'auto' }}>
531
+ <CloseIcon />
532
+ </IconButton>
533
+ </DialogTitle>
534
+ );
535
+ }
536
+
537
+ if (!segment) return null;
538
+
539
+ return (
540
+ <DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
541
+ <Box sx={{ flex: 1, display: 'flex', alignItems: 'center', gap: 1 }}>
542
+ Edit {isGlobal ? 'All Words' : `Segment ${segmentIndex}`}
543
+ {segment?.start_time !== null && onPlaySegment && (
544
+ <IconButton
545
+ size="small"
546
+ onClick={handlePlayButtonClick}
547
+ sx={{ padding: '4px' }}
548
+ >
549
+ {isPlaying ? (
550
+ <StopIcon />
551
+ ) : (
552
+ <PlayCircleOutlineIcon />
553
+ )}
554
+ </IconButton>
555
+ )}
556
+ </Box>
557
+ <IconButton onClick={onClose} sx={{ ml: 'auto' }}>
558
+ <CloseIcon />
559
+ </IconButton>
560
+ </DialogTitle>
561
+ );
562
+ }, [isGlobal, segmentIndex, segment, onPlaySegment, handlePlayButtonClick, isPlaying, onClose, isLoading])
563
+
564
+ // Early return after all hooks and function definitions
565
+ if (!isLoading && (!segment || !editedSegment || !originalSegment)) {
566
+ // console.log('EditModal - Early return: missing required data', {
567
+ // hasSegment: !!segment,
568
+ // hasEditedSegment: !!editedSegment,
569
+ // hasOriginalSegment: !!originalSegment,
570
+ // isLoading
571
+ // });
572
+ return null;
573
+ }
574
+ if (!isLoading && !isGlobal && segmentIndex === null) {
575
+ // console.log('EditModal - Early return: non-global mode with null segmentIndex');
576
+ return null;
577
+ }
578
+
579
+ // console.log('EditModal - Rendering dialog content', {
580
+ // isLoading,
581
+ // hasEditedSegment: !!editedSegment,
582
+ // hasOriginalSegment: !!originalSegment
583
+ // });
584
+
585
+ return (
586
+ <Dialog
587
+ open={open}
588
+ onClose={handleClose}
589
+ maxWidth="md"
590
+ fullWidth
591
+ onKeyDown={(e) => {
592
+ if (e.key === 'Enter' && !e.shiftKey && !isLoading) {
593
+ e.preventDefault()
594
+ handleSave()
595
+ }
596
+ }}
597
+ PaperProps={{
598
+ sx: {
599
+ height: '90vh',
600
+ margin: '5vh 0'
601
+ }
602
+ }}
603
+ >
604
+ {dialogTitle}
605
+
606
+ <DialogContent
607
+ dividers
608
+ sx={{
609
+ display: 'flex',
610
+ flexDirection: 'column',
611
+ flexGrow: 1,
612
+ overflow: 'hidden',
613
+ position: 'relative'
614
+ }}
615
+ >
616
+ {isLoading && (
617
+ <Box sx={{
618
+ display: 'flex',
619
+ flexDirection: 'column',
620
+ alignItems: 'center',
621
+ justifyContent: 'center',
622
+ height: '100%',
623
+ width: '100%',
624
+ position: 'absolute',
625
+ top: 0,
626
+ left: 0,
627
+ backgroundColor: 'rgba(255, 255, 255, 0.9)',
628
+ zIndex: 10
629
+ }}>
630
+ <CircularProgress size={60} thickness={4} />
631
+ <Typography variant="h6" sx={{ mt: 2, fontWeight: 'bold' }}>
632
+ Loading {isGlobal ? 'all words' : 'segment'}...
633
+ </Typography>
634
+ <Typography variant="body2" sx={{ mt: 1, maxWidth: '80%', textAlign: 'center' }}>
635
+ {isGlobal ? 'This may take a few seconds for songs with many words.' : 'Please wait...'}
636
+ </Typography>
637
+ </Box>
638
+ )}
639
+
640
+ {!isLoading && editedSegment && originalSegment && (
641
+ <>
642
+ <MemoizedTimelineSection
643
+ words={editedSegment.words}
644
+ timeRange={timeRange}
645
+ originalSegment={originalSegment}
646
+ editedSegment={editedSegment}
647
+ currentTime={currentTime}
648
+ isManualSyncing={isManualSyncing}
649
+ syncWordIndex={syncWordIndex}
650
+ isSpacebarPressed={isSpacebarPressed}
651
+ onWordUpdate={handleWordChange}
652
+ onPlaySegment={onPlaySegment}
653
+ startManualSync={startManualSync}
654
+ isGlobal={isGlobal}
655
+ />
656
+
657
+ <MemoizedWordList
658
+ words={editedSegment.words}
659
+ onWordUpdate={handleWordChange}
660
+ onSplitWord={handleSplitWord}
661
+ onMergeWords={handleMergeWords}
662
+ onAddWord={handleAddWord}
663
+ onRemoveWord={handleRemoveWord}
664
+ onSplitSegment={handleSplitSegment}
665
+ onAddSegment={onAddSegment}
666
+ onMergeSegment={handleMergeSegment}
667
+ isGlobal={isGlobal}
668
+ />
669
+ </>
670
+ )}
671
+
672
+ {!isLoading && (!editedSegment || !originalSegment) && (
673
+ <Box sx={{
674
+ display: 'flex',
675
+ alignItems: 'center',
676
+ justifyContent: 'center',
677
+ height: '100%'
678
+ }}>
679
+ <Typography variant="h6">
680
+ No segment data available
681
+ </Typography>
682
+ </Box>
683
+ )}
684
+ </DialogContent>
685
+
686
+ <DialogActions>
687
+ {!isLoading && editedSegment && (
688
+ <MemoizedActionBar
689
+ onReset={handleReset}
690
+ onRevertToOriginal={handleRevertToOriginal}
691
+ onDelete={handleDelete}
692
+ onClose={handleClose}
693
+ onSave={handleSave}
694
+ editedSegment={editedSegment}
695
+ originalTranscribedSegment={originalTranscribedSegment}
696
+ isGlobal={isGlobal}
697
+ />
698
+ )}
699
+ </DialogActions>
700
+ </Dialog>
701
+ )
702
+ }