karaoke-gen 0.57.0__py3-none-any.whl → 0.71.27__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. karaoke_gen/audio_fetcher.py +461 -0
  2. karaoke_gen/audio_processor.py +407 -30
  3. karaoke_gen/config.py +62 -113
  4. karaoke_gen/file_handler.py +32 -59
  5. karaoke_gen/karaoke_finalise/karaoke_finalise.py +148 -67
  6. karaoke_gen/karaoke_gen.py +270 -61
  7. karaoke_gen/lyrics_processor.py +13 -1
  8. karaoke_gen/metadata.py +78 -73
  9. karaoke_gen/pipeline/__init__.py +87 -0
  10. karaoke_gen/pipeline/base.py +215 -0
  11. karaoke_gen/pipeline/context.py +230 -0
  12. karaoke_gen/pipeline/executors/__init__.py +21 -0
  13. karaoke_gen/pipeline/executors/local.py +159 -0
  14. karaoke_gen/pipeline/executors/remote.py +257 -0
  15. karaoke_gen/pipeline/stages/__init__.py +27 -0
  16. karaoke_gen/pipeline/stages/finalize.py +202 -0
  17. karaoke_gen/pipeline/stages/render.py +165 -0
  18. karaoke_gen/pipeline/stages/screens.py +139 -0
  19. karaoke_gen/pipeline/stages/separation.py +191 -0
  20. karaoke_gen/pipeline/stages/transcription.py +191 -0
  21. karaoke_gen/style_loader.py +531 -0
  22. karaoke_gen/utils/bulk_cli.py +6 -0
  23. karaoke_gen/utils/cli_args.py +424 -0
  24. karaoke_gen/utils/gen_cli.py +26 -261
  25. karaoke_gen/utils/remote_cli.py +1965 -0
  26. karaoke_gen/video_background_processor.py +351 -0
  27. karaoke_gen-0.71.27.dist-info/METADATA +610 -0
  28. karaoke_gen-0.71.27.dist-info/RECORD +275 -0
  29. {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.27.dist-info}/WHEEL +1 -1
  30. {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.27.dist-info}/entry_points.txt +1 -0
  31. lyrics_transcriber/__init__.py +10 -0
  32. lyrics_transcriber/cli/__init__.py +0 -0
  33. lyrics_transcriber/cli/cli_main.py +285 -0
  34. lyrics_transcriber/core/__init__.py +0 -0
  35. lyrics_transcriber/core/config.py +50 -0
  36. lyrics_transcriber/core/controller.py +520 -0
  37. lyrics_transcriber/correction/__init__.py +0 -0
  38. lyrics_transcriber/correction/agentic/__init__.py +9 -0
  39. lyrics_transcriber/correction/agentic/adapter.py +71 -0
  40. lyrics_transcriber/correction/agentic/agent.py +313 -0
  41. lyrics_transcriber/correction/agentic/feedback/aggregator.py +12 -0
  42. lyrics_transcriber/correction/agentic/feedback/collector.py +17 -0
  43. lyrics_transcriber/correction/agentic/feedback/retention.py +24 -0
  44. lyrics_transcriber/correction/agentic/feedback/store.py +76 -0
  45. lyrics_transcriber/correction/agentic/handlers/__init__.py +24 -0
  46. lyrics_transcriber/correction/agentic/handlers/ambiguous.py +44 -0
  47. lyrics_transcriber/correction/agentic/handlers/background_vocals.py +68 -0
  48. lyrics_transcriber/correction/agentic/handlers/base.py +51 -0
  49. lyrics_transcriber/correction/agentic/handlers/complex_multi_error.py +46 -0
  50. lyrics_transcriber/correction/agentic/handlers/extra_words.py +74 -0
  51. lyrics_transcriber/correction/agentic/handlers/no_error.py +42 -0
  52. lyrics_transcriber/correction/agentic/handlers/punctuation.py +44 -0
  53. lyrics_transcriber/correction/agentic/handlers/registry.py +60 -0
  54. lyrics_transcriber/correction/agentic/handlers/repeated_section.py +44 -0
  55. lyrics_transcriber/correction/agentic/handlers/sound_alike.py +126 -0
  56. lyrics_transcriber/correction/agentic/models/__init__.py +5 -0
  57. lyrics_transcriber/correction/agentic/models/ai_correction.py +31 -0
  58. lyrics_transcriber/correction/agentic/models/correction_session.py +30 -0
  59. lyrics_transcriber/correction/agentic/models/enums.py +38 -0
  60. lyrics_transcriber/correction/agentic/models/human_feedback.py +30 -0
  61. lyrics_transcriber/correction/agentic/models/learning_data.py +26 -0
  62. lyrics_transcriber/correction/agentic/models/observability_metrics.py +28 -0
  63. lyrics_transcriber/correction/agentic/models/schemas.py +46 -0
  64. lyrics_transcriber/correction/agentic/models/utils.py +19 -0
  65. lyrics_transcriber/correction/agentic/observability/__init__.py +5 -0
  66. lyrics_transcriber/correction/agentic/observability/langfuse_integration.py +35 -0
  67. lyrics_transcriber/correction/agentic/observability/metrics.py +46 -0
  68. lyrics_transcriber/correction/agentic/observability/performance.py +19 -0
  69. lyrics_transcriber/correction/agentic/prompts/__init__.py +2 -0
  70. lyrics_transcriber/correction/agentic/prompts/classifier.py +227 -0
  71. lyrics_transcriber/correction/agentic/providers/__init__.py +6 -0
  72. lyrics_transcriber/correction/agentic/providers/base.py +36 -0
  73. lyrics_transcriber/correction/agentic/providers/circuit_breaker.py +145 -0
  74. lyrics_transcriber/correction/agentic/providers/config.py +73 -0
  75. lyrics_transcriber/correction/agentic/providers/constants.py +24 -0
  76. lyrics_transcriber/correction/agentic/providers/health.py +28 -0
  77. lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +212 -0
  78. lyrics_transcriber/correction/agentic/providers/model_factory.py +209 -0
  79. lyrics_transcriber/correction/agentic/providers/response_cache.py +218 -0
  80. lyrics_transcriber/correction/agentic/providers/response_parser.py +111 -0
  81. lyrics_transcriber/correction/agentic/providers/retry_executor.py +127 -0
  82. lyrics_transcriber/correction/agentic/router.py +35 -0
  83. lyrics_transcriber/correction/agentic/workflows/__init__.py +5 -0
  84. lyrics_transcriber/correction/agentic/workflows/consensus_workflow.py +24 -0
  85. lyrics_transcriber/correction/agentic/workflows/correction_graph.py +59 -0
  86. lyrics_transcriber/correction/agentic/workflows/feedback_workflow.py +24 -0
  87. lyrics_transcriber/correction/anchor_sequence.py +1043 -0
  88. lyrics_transcriber/correction/corrector.py +760 -0
  89. lyrics_transcriber/correction/feedback/__init__.py +2 -0
  90. lyrics_transcriber/correction/feedback/schemas.py +107 -0
  91. lyrics_transcriber/correction/feedback/store.py +236 -0
  92. lyrics_transcriber/correction/handlers/__init__.py +0 -0
  93. lyrics_transcriber/correction/handlers/base.py +52 -0
  94. lyrics_transcriber/correction/handlers/extend_anchor.py +149 -0
  95. lyrics_transcriber/correction/handlers/levenshtein.py +189 -0
  96. lyrics_transcriber/correction/handlers/llm.py +293 -0
  97. lyrics_transcriber/correction/handlers/llm_providers.py +60 -0
  98. lyrics_transcriber/correction/handlers/no_space_punct_match.py +154 -0
  99. lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +85 -0
  100. lyrics_transcriber/correction/handlers/repeat.py +88 -0
  101. lyrics_transcriber/correction/handlers/sound_alike.py +259 -0
  102. lyrics_transcriber/correction/handlers/syllables_match.py +252 -0
  103. lyrics_transcriber/correction/handlers/word_count_match.py +80 -0
  104. lyrics_transcriber/correction/handlers/word_operations.py +187 -0
  105. lyrics_transcriber/correction/operations.py +352 -0
  106. lyrics_transcriber/correction/phrase_analyzer.py +435 -0
  107. lyrics_transcriber/correction/text_utils.py +30 -0
  108. lyrics_transcriber/frontend/.gitignore +23 -0
  109. lyrics_transcriber/frontend/.yarn/releases/yarn-4.7.0.cjs +935 -0
  110. lyrics_transcriber/frontend/.yarnrc.yml +3 -0
  111. lyrics_transcriber/frontend/README.md +50 -0
  112. lyrics_transcriber/frontend/REPLACE_ALL_FUNCTIONALITY.md +210 -0
  113. lyrics_transcriber/frontend/__init__.py +25 -0
  114. lyrics_transcriber/frontend/eslint.config.js +28 -0
  115. lyrics_transcriber/frontend/index.html +18 -0
  116. lyrics_transcriber/frontend/package.json +42 -0
  117. lyrics_transcriber/frontend/public/android-chrome-192x192.png +0 -0
  118. lyrics_transcriber/frontend/public/android-chrome-512x512.png +0 -0
  119. lyrics_transcriber/frontend/public/apple-touch-icon.png +0 -0
  120. lyrics_transcriber/frontend/public/favicon-16x16.png +0 -0
  121. lyrics_transcriber/frontend/public/favicon-32x32.png +0 -0
  122. lyrics_transcriber/frontend/public/favicon.ico +0 -0
  123. lyrics_transcriber/frontend/public/nomad-karaoke-logo.png +0 -0
  124. lyrics_transcriber/frontend/src/App.tsx +212 -0
  125. lyrics_transcriber/frontend/src/api.ts +239 -0
  126. lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +77 -0
  127. lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx +114 -0
  128. lyrics_transcriber/frontend/src/components/AgenticCorrectionMetrics.tsx +204 -0
  129. lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +180 -0
  130. lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +167 -0
  131. lyrics_transcriber/frontend/src/components/CorrectionAnnotationModal.tsx +359 -0
  132. lyrics_transcriber/frontend/src/components/CorrectionDetailCard.tsx +281 -0
  133. lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +162 -0
  134. lyrics_transcriber/frontend/src/components/DurationTimelineView.tsx +257 -0
  135. lyrics_transcriber/frontend/src/components/EditActionBar.tsx +68 -0
  136. lyrics_transcriber/frontend/src/components/EditModal.tsx +702 -0
  137. lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +496 -0
  138. lyrics_transcriber/frontend/src/components/EditWordList.tsx +379 -0
  139. lyrics_transcriber/frontend/src/components/FileUpload.tsx +77 -0
  140. lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +467 -0
  141. lyrics_transcriber/frontend/src/components/Header.tsx +387 -0
  142. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +1373 -0
  143. lyrics_transcriber/frontend/src/components/MetricsDashboard.tsx +51 -0
  144. lyrics_transcriber/frontend/src/components/ModeSelector.tsx +67 -0
  145. lyrics_transcriber/frontend/src/components/ModelSelector.tsx +23 -0
  146. lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +144 -0
  147. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +268 -0
  148. lyrics_transcriber/frontend/src/components/ReplaceAllLyricsModal.tsx +688 -0
  149. lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +354 -0
  150. lyrics_transcriber/frontend/src/components/SegmentDetailsModal.tsx +64 -0
  151. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +376 -0
  152. lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +131 -0
  153. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +256 -0
  154. lyrics_transcriber/frontend/src/components/WordDivider.tsx +187 -0
  155. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +379 -0
  156. lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +56 -0
  157. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +87 -0
  158. lyrics_transcriber/frontend/src/components/shared/constants.ts +20 -0
  159. lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +180 -0
  160. lyrics_transcriber/frontend/src/components/shared/styles.ts +13 -0
  161. lyrics_transcriber/frontend/src/components/shared/types.js +2 -0
  162. lyrics_transcriber/frontend/src/components/shared/types.ts +129 -0
  163. lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +177 -0
  164. lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +78 -0
  165. lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +75 -0
  166. lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +360 -0
  167. lyrics_transcriber/frontend/src/components/shared/utils/timingUtils.ts +110 -0
  168. lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +22 -0
  169. lyrics_transcriber/frontend/src/hooks/useManualSync.ts +435 -0
  170. lyrics_transcriber/frontend/src/main.tsx +17 -0
  171. lyrics_transcriber/frontend/src/theme.ts +177 -0
  172. lyrics_transcriber/frontend/src/types/global.d.ts +9 -0
  173. lyrics_transcriber/frontend/src/types.js +2 -0
  174. lyrics_transcriber/frontend/src/types.ts +199 -0
  175. lyrics_transcriber/frontend/src/validation.ts +132 -0
  176. lyrics_transcriber/frontend/src/vite-env.d.ts +1 -0
  177. lyrics_transcriber/frontend/tsconfig.app.json +26 -0
  178. lyrics_transcriber/frontend/tsconfig.json +25 -0
  179. lyrics_transcriber/frontend/tsconfig.node.json +23 -0
  180. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -0
  181. lyrics_transcriber/frontend/update_version.js +11 -0
  182. lyrics_transcriber/frontend/vite.config.d.ts +2 -0
  183. lyrics_transcriber/frontend/vite.config.js +10 -0
  184. lyrics_transcriber/frontend/vite.config.ts +11 -0
  185. lyrics_transcriber/frontend/web_assets/android-chrome-192x192.png +0 -0
  186. lyrics_transcriber/frontend/web_assets/android-chrome-512x512.png +0 -0
  187. lyrics_transcriber/frontend/web_assets/apple-touch-icon.png +0 -0
  188. lyrics_transcriber/frontend/web_assets/assets/index-DdJTDWH3.js +42039 -0
  189. lyrics_transcriber/frontend/web_assets/assets/index-DdJTDWH3.js.map +1 -0
  190. lyrics_transcriber/frontend/web_assets/favicon-16x16.png +0 -0
  191. lyrics_transcriber/frontend/web_assets/favicon-32x32.png +0 -0
  192. lyrics_transcriber/frontend/web_assets/favicon.ico +0 -0
  193. lyrics_transcriber/frontend/web_assets/index.html +18 -0
  194. lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.png +0 -0
  195. lyrics_transcriber/frontend/yarn.lock +3752 -0
  196. lyrics_transcriber/lyrics/__init__.py +0 -0
  197. lyrics_transcriber/lyrics/base_lyrics_provider.py +211 -0
  198. lyrics_transcriber/lyrics/file_provider.py +95 -0
  199. lyrics_transcriber/lyrics/genius.py +384 -0
  200. lyrics_transcriber/lyrics/lrclib.py +231 -0
  201. lyrics_transcriber/lyrics/musixmatch.py +156 -0
  202. lyrics_transcriber/lyrics/spotify.py +290 -0
  203. lyrics_transcriber/lyrics/user_input_provider.py +44 -0
  204. lyrics_transcriber/output/__init__.py +0 -0
  205. lyrics_transcriber/output/ass/__init__.py +21 -0
  206. lyrics_transcriber/output/ass/ass.py +2088 -0
  207. lyrics_transcriber/output/ass/ass_specs.txt +732 -0
  208. lyrics_transcriber/output/ass/config.py +180 -0
  209. lyrics_transcriber/output/ass/constants.py +23 -0
  210. lyrics_transcriber/output/ass/event.py +94 -0
  211. lyrics_transcriber/output/ass/formatters.py +132 -0
  212. lyrics_transcriber/output/ass/lyrics_line.py +265 -0
  213. lyrics_transcriber/output/ass/lyrics_screen.py +252 -0
  214. lyrics_transcriber/output/ass/section_detector.py +89 -0
  215. lyrics_transcriber/output/ass/section_screen.py +106 -0
  216. lyrics_transcriber/output/ass/style.py +187 -0
  217. lyrics_transcriber/output/cdg.py +619 -0
  218. lyrics_transcriber/output/cdgmaker/__init__.py +0 -0
  219. lyrics_transcriber/output/cdgmaker/cdg.py +262 -0
  220. lyrics_transcriber/output/cdgmaker/composer.py +2260 -0
  221. lyrics_transcriber/output/cdgmaker/config.py +151 -0
  222. lyrics_transcriber/output/cdgmaker/images/instrumental.png +0 -0
  223. lyrics_transcriber/output/cdgmaker/images/intro.png +0 -0
  224. lyrics_transcriber/output/cdgmaker/pack.py +507 -0
  225. lyrics_transcriber/output/cdgmaker/render.py +346 -0
  226. lyrics_transcriber/output/cdgmaker/transitions/centertexttoplogobottomtext.png +0 -0
  227. lyrics_transcriber/output/cdgmaker/transitions/circlein.png +0 -0
  228. lyrics_transcriber/output/cdgmaker/transitions/circleout.png +0 -0
  229. lyrics_transcriber/output/cdgmaker/transitions/fizzle.png +0 -0
  230. lyrics_transcriber/output/cdgmaker/transitions/largecentertexttoplogo.png +0 -0
  231. lyrics_transcriber/output/cdgmaker/transitions/rectangle.png +0 -0
  232. lyrics_transcriber/output/cdgmaker/transitions/spiral.png +0 -0
  233. lyrics_transcriber/output/cdgmaker/transitions/topleftmusicalnotes.png +0 -0
  234. lyrics_transcriber/output/cdgmaker/transitions/wipein.png +0 -0
  235. lyrics_transcriber/output/cdgmaker/transitions/wipeleft.png +0 -0
  236. lyrics_transcriber/output/cdgmaker/transitions/wipeout.png +0 -0
  237. lyrics_transcriber/output/cdgmaker/transitions/wiperight.png +0 -0
  238. lyrics_transcriber/output/cdgmaker/utils.py +132 -0
  239. lyrics_transcriber/output/countdown_processor.py +267 -0
  240. lyrics_transcriber/output/fonts/AvenirNext-Bold.ttf +0 -0
  241. lyrics_transcriber/output/fonts/DMSans-VariableFont_opsz,wght.ttf +0 -0
  242. lyrics_transcriber/output/fonts/DMSerifDisplay-Regular.ttf +0 -0
  243. lyrics_transcriber/output/fonts/Oswald-SemiBold.ttf +0 -0
  244. lyrics_transcriber/output/fonts/Zurich_Cn_BT_Bold.ttf +0 -0
  245. lyrics_transcriber/output/fonts/arial.ttf +0 -0
  246. lyrics_transcriber/output/fonts/georgia.ttf +0 -0
  247. lyrics_transcriber/output/fonts/verdana.ttf +0 -0
  248. lyrics_transcriber/output/generator.py +257 -0
  249. lyrics_transcriber/output/lrc_to_cdg.py +61 -0
  250. lyrics_transcriber/output/lyrics_file.py +102 -0
  251. lyrics_transcriber/output/plain_text.py +96 -0
  252. lyrics_transcriber/output/segment_resizer.py +431 -0
  253. lyrics_transcriber/output/subtitles.py +397 -0
  254. lyrics_transcriber/output/video.py +544 -0
  255. lyrics_transcriber/review/__init__.py +0 -0
  256. lyrics_transcriber/review/server.py +676 -0
  257. lyrics_transcriber/storage/__init__.py +0 -0
  258. lyrics_transcriber/storage/dropbox.py +225 -0
  259. lyrics_transcriber/transcribers/__init__.py +0 -0
  260. lyrics_transcriber/transcribers/audioshake.py +290 -0
  261. lyrics_transcriber/transcribers/base_transcriber.py +157 -0
  262. lyrics_transcriber/transcribers/whisper.py +330 -0
  263. lyrics_transcriber/types.py +648 -0
  264. lyrics_transcriber/utils/__init__.py +0 -0
  265. lyrics_transcriber/utils/word_utils.py +27 -0
  266. karaoke_gen-0.57.0.dist-info/METADATA +0 -167
  267. karaoke_gen-0.57.0.dist-info/RECORD +0 -23
  268. {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.27.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,379 @@
1
+ import {
2
+ Box,
3
+ TextField,
4
+ IconButton,
5
+ Button,
6
+ Pagination,
7
+ Typography
8
+ } from '@mui/material'
9
+ import DeleteIcon from '@mui/icons-material/Delete'
10
+ import SplitIcon from '@mui/icons-material/CallSplit'
11
+ import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh'
12
+ import { Word } from '../types'
13
+ import { useState, memo, useMemo } from 'react'
14
+ import WordDivider from './WordDivider'
15
+
16
+ interface EditWordListProps {
17
+ words: Word[]
18
+ onWordUpdate: (index: number, updates: Partial<Word>) => void
19
+ onSplitWord: (index: number) => void
20
+ onMergeWords: (index: number) => void
21
+ onAddWord: (index?: number) => void
22
+ onRemoveWord: (index: number) => void
23
+ onSplitSegment?: (wordIndex: number) => void
24
+ onAddSegment?: (beforeIndex: number) => void
25
+ onMergeSegment?: (mergeWithNext: boolean) => void
26
+ isGlobal?: boolean
27
+ }
28
+
29
+ // Create a memoized word row component to prevent re-renders
30
+ const WordRow = memo(function WordRow({
31
+ word,
32
+ index,
33
+ onWordUpdate,
34
+ onSplitWord,
35
+ onRemoveWord,
36
+ wordsLength,
37
+ onTabNavigation
38
+ }: {
39
+ word: Word
40
+ index: number
41
+ onWordUpdate: (index: number, updates: Partial<Word>) => void
42
+ onSplitWord: (index: number) => void
43
+ onRemoveWord: (index: number) => void
44
+ wordsLength: number
45
+ onTabNavigation: (currentIndex: number) => void
46
+ }) {
47
+ const handleKeyDown = (e: React.KeyboardEvent) => {
48
+ // console.log('KeyDown event:', e.key, 'Shift:', e.shiftKey, 'Index:', index);
49
+ if (e.key === 'Tab' && !e.shiftKey) {
50
+ // console.log('Tab key detected, preventing default and navigating');
51
+ e.preventDefault();
52
+ onTabNavigation(index);
53
+ }
54
+ };
55
+
56
+ return (
57
+ <Box sx={{
58
+ display: 'flex',
59
+ gap: 2,
60
+ alignItems: 'center',
61
+ padding: '4px 0',
62
+ }}>
63
+ <TextField
64
+ label={`Word ${index}`}
65
+ value={word.text}
66
+ onChange={(e) => onWordUpdate(index, { text: e.target.value })}
67
+ onKeyDown={handleKeyDown}
68
+ fullWidth
69
+ size="small"
70
+ id={`word-text-${index}`}
71
+ />
72
+ <TextField
73
+ label="Start Time"
74
+ value={word.start_time?.toFixed(2) ?? ''}
75
+ onChange={(e) => onWordUpdate(index, { start_time: parseFloat(e.target.value) })}
76
+ type="number"
77
+ inputProps={{ step: 0.01 }}
78
+ sx={{ width: '150px' }}
79
+ size="small"
80
+ />
81
+ <TextField
82
+ label="End Time"
83
+ value={word.end_time?.toFixed(2) ?? ''}
84
+ onChange={(e) => onWordUpdate(index, { end_time: parseFloat(e.target.value) })}
85
+ type="number"
86
+ inputProps={{ step: 0.01 }}
87
+ sx={{ width: '150px' }}
88
+ size="small"
89
+ />
90
+ <IconButton
91
+ onClick={() => onSplitWord(index)}
92
+ title="Split Word"
93
+ sx={{ color: 'primary.main' }}
94
+ size="small"
95
+ >
96
+ <SplitIcon fontSize="small" />
97
+ </IconButton>
98
+ <IconButton
99
+ onClick={() => onRemoveWord(index)}
100
+ disabled={wordsLength <= 1}
101
+ title="Remove Word"
102
+ sx={{ color: 'error.main' }}
103
+ size="small"
104
+ >
105
+ <DeleteIcon fontSize="small" />
106
+ </IconButton>
107
+ </Box>
108
+ );
109
+ });
110
+
111
+ // Memoized word item component that includes the word row and divider
112
+ const WordItem = memo(function WordItem({
113
+ word,
114
+ index,
115
+ onWordUpdate,
116
+ onSplitWord,
117
+ onRemoveWord,
118
+ onAddWord,
119
+ onMergeWords,
120
+ onSplitSegment,
121
+ onAddSegment,
122
+ onMergeSegment,
123
+ wordsLength,
124
+ isGlobal,
125
+ onTabNavigation
126
+ }: {
127
+ word: Word
128
+ index: number
129
+ onWordUpdate: (index: number, updates: Partial<Word>) => void
130
+ onSplitWord: (index: number) => void
131
+ onRemoveWord: (index: number) => void
132
+ onAddWord: (index: number) => void
133
+ onMergeWords: (index: number) => void
134
+ onSplitSegment?: (index: number) => void
135
+ onAddSegment?: (index: number) => void
136
+ onMergeSegment?: (mergeWithNext: boolean) => void
137
+ wordsLength: number
138
+ isGlobal: boolean
139
+ onTabNavigation: (currentIndex: number) => void
140
+ }) {
141
+ return (
142
+ <Box key={word.id}>
143
+ <WordRow
144
+ word={word}
145
+ index={index}
146
+ onWordUpdate={onWordUpdate}
147
+ onSplitWord={onSplitWord}
148
+ onRemoveWord={onRemoveWord}
149
+ wordsLength={wordsLength}
150
+ onTabNavigation={onTabNavigation}
151
+ />
152
+
153
+ {/* Word divider with merge/split functionality */}
154
+ {!isGlobal && (
155
+ <WordDivider
156
+ onAddWord={() => onAddWord(index)}
157
+ onMergeWords={() => onMergeWords(index)}
158
+ onSplitSegment={() => onSplitSegment?.(index)}
159
+ onAddSegmentAfter={
160
+ index === wordsLength - 1
161
+ ? () => onAddSegment?.(index + 1)
162
+ : undefined
163
+ }
164
+ onMergeSegment={
165
+ index === wordsLength - 1
166
+ ? () => onMergeSegment?.(true)
167
+ : undefined
168
+ }
169
+ canMerge={index < wordsLength - 1}
170
+ isLast={index === wordsLength - 1}
171
+ sx={{ ml: 15 }}
172
+ />
173
+ )}
174
+ {isGlobal && (
175
+ <WordDivider
176
+ onAddWord={() => onAddWord(index)}
177
+ onMergeWords={index < wordsLength - 1 ? () => onMergeWords(index) : undefined}
178
+ canMerge={index < wordsLength - 1}
179
+ sx={{ ml: 15 }}
180
+ />
181
+ )}
182
+ </Box>
183
+ );
184
+ });
185
+
186
+ export default function EditWordList({
187
+ words,
188
+ onWordUpdate,
189
+ onSplitWord,
190
+ onMergeWords,
191
+ onAddWord,
192
+ onRemoveWord,
193
+ onSplitSegment,
194
+ onAddSegment,
195
+ onMergeSegment,
196
+ isGlobal = false
197
+ }: EditWordListProps) {
198
+ const [replacementText, setReplacementText] = useState('')
199
+ const [page, setPage] = useState(1)
200
+ const pageSize = isGlobal ? 50 : words.length // Use pagination only in global mode
201
+
202
+ const handleReplaceAllWords = () => {
203
+ const newWords = replacementText.trim().split(/\s+/)
204
+ newWords.forEach((text, index) => {
205
+ if (index < words.length) {
206
+ onWordUpdate(index, { text })
207
+ }
208
+ })
209
+ setReplacementText('')
210
+ }
211
+
212
+ // Calculate pagination values
213
+ const pageCount = Math.ceil(words.length / pageSize)
214
+ const startIndex = (page - 1) * pageSize
215
+ const endIndex = Math.min(startIndex + pageSize, words.length)
216
+
217
+ // Get the words for the current page
218
+ const visibleWords = useMemo(() => {
219
+ return isGlobal
220
+ ? words.slice(startIndex, endIndex)
221
+ : words;
222
+ }, [words, isGlobal, startIndex, endIndex]);
223
+
224
+ // Handle page change
225
+ const handlePageChange = (_event: React.ChangeEvent<unknown>, value: number) => {
226
+ setPage(value);
227
+ };
228
+
229
+ // Handle tab navigation between word text fields
230
+ const handleTabNavigation = (currentIndex: number) => {
231
+ // console.log('handleTabNavigation called with index:', currentIndex);
232
+ const nextIndex = (currentIndex + 1) % words.length;
233
+ // console.log('Next index calculated:', nextIndex, 'Total words:', words.length);
234
+
235
+ // If the next word is on a different page, change the page
236
+ if (isGlobal && (nextIndex < startIndex || nextIndex >= endIndex)) {
237
+ // console.log('Next word is on different page. Current page:', page, 'startIndex:', startIndex, 'endIndex:', endIndex);
238
+ const nextPage = Math.floor(nextIndex / pageSize) + 1;
239
+ // console.log('Changing to page:', nextPage);
240
+ setPage(nextPage);
241
+
242
+ // Use setTimeout to allow the page change to render before focusing
243
+ setTimeout(() => {
244
+ // console.log('Timeout callback executing, trying to focus element with ID:', `word-text-${nextIndex}`);
245
+ focusWordTextField(nextIndex);
246
+ }, 50);
247
+ } else {
248
+ // console.log('Next word is on same page, trying to focus element with ID:', `word-text-${nextIndex}`);
249
+ focusWordTextField(nextIndex);
250
+ }
251
+ };
252
+
253
+ // Helper function to focus a word text field by index
254
+ const focusWordTextField = (index: number) => {
255
+ // Material-UI TextField uses a more complex structure
256
+ // The actual input is inside the TextField component
257
+ const element = document.getElementById(`word-text-${index}`);
258
+ // console.log('Element found:', !!element);
259
+
260
+ if (element) {
261
+ // Try different selectors to find the input element
262
+ // First try the standard input selector
263
+ let input = element.querySelector('input');
264
+
265
+ // If that doesn't work, try the MUI-specific selector
266
+ if (!input) {
267
+ input = element.querySelector('.MuiInputBase-input');
268
+ }
269
+
270
+ // console.log('Input element found:', !!input);
271
+ if (input) {
272
+ input.focus();
273
+ input.select();
274
+ // console.log('Focus and select called on input');
275
+ } else {
276
+ // As a fallback, try to focus the TextField itself
277
+ // console.log('Trying to focus the TextField itself');
278
+ element.focus();
279
+ }
280
+ }
281
+ };
282
+
283
+ return (
284
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, flexGrow: 1, minHeight: 0 }}>
285
+ {/* Initial divider with Add Segment Before button */}
286
+ {!isGlobal && (
287
+ <WordDivider
288
+ onAddWord={() => onAddWord(-1)}
289
+ onAddSegmentBefore={() => onAddSegment?.(0)}
290
+ onMergeSegment={() => onMergeSegment?.(false)}
291
+ isFirst={true}
292
+ sx={{ ml: 15 }}
293
+ />
294
+ )}
295
+ {isGlobal && (
296
+ <WordDivider
297
+ onAddWord={() => onAddWord(-1)}
298
+ sx={{ ml: 15 }}
299
+ />
300
+ )}
301
+
302
+ {/* Word list with scrolling */}
303
+ <Box sx={{
304
+ display: 'flex',
305
+ flexDirection: 'column',
306
+ gap: 0.5,
307
+ flexGrow: 1,
308
+ overflowY: 'auto',
309
+ mb: 0,
310
+ pt: 1,
311
+ '&::-webkit-scrollbar': {
312
+ width: '8px',
313
+ },
314
+ '&::-webkit-scrollbar-thumb': {
315
+ backgroundColor: 'rgba(0,0,0,0.2)',
316
+ borderRadius: '4px',
317
+ },
318
+ scrollbarWidth: 'thin',
319
+ msOverflowStyle: 'autohiding-scrollbar',
320
+ }}>
321
+ {visibleWords.map((word, visibleIndex) => {
322
+ const actualIndex = isGlobal ? startIndex + visibleIndex : visibleIndex;
323
+ return (
324
+ <WordItem
325
+ key={word.id}
326
+ word={word}
327
+ index={actualIndex}
328
+ onWordUpdate={onWordUpdate}
329
+ onSplitWord={onSplitWord}
330
+ onRemoveWord={onRemoveWord}
331
+ onAddWord={onAddWord}
332
+ onMergeWords={onMergeWords}
333
+ onSplitSegment={onSplitSegment}
334
+ onAddSegment={onAddSegment}
335
+ onMergeSegment={onMergeSegment}
336
+ wordsLength={words.length}
337
+ isGlobal={isGlobal}
338
+ onTabNavigation={handleTabNavigation}
339
+ />
340
+ );
341
+ })}
342
+ </Box>
343
+
344
+ {/* Pagination controls (only in global mode) */}
345
+ {isGlobal && pageCount > 1 && (
346
+ <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', mb: 1 }}>
347
+ <Pagination
348
+ count={pageCount}
349
+ page={page}
350
+ onChange={handlePageChange}
351
+ color="primary"
352
+ size="small"
353
+ />
354
+ <Typography variant="body2" sx={{ ml: 2 }}>
355
+ Showing words {startIndex + 1}-{endIndex} of {words.length}
356
+ </Typography>
357
+ </Box>
358
+ )}
359
+
360
+ <Box sx={{ display: 'flex', gap: 2, mb: 0.6 }}>
361
+ <TextField
362
+ value={replacementText}
363
+ onChange={(e) => setReplacementText(e.target.value)}
364
+ placeholder="Replace all words"
365
+ size="small"
366
+ sx={{ flexGrow: 1, maxWidth: 'calc(100% - 140px)' }}
367
+ />
368
+ <Button
369
+ onClick={handleReplaceAllWords}
370
+ startIcon={<AutoFixHighIcon />}
371
+ size="small"
372
+ sx={{ whiteSpace: 'nowrap' }}
373
+ >
374
+ Replace All
375
+ </Button>
376
+ </Box>
377
+ </Box>
378
+ )
379
+ }
@@ -0,0 +1,77 @@
1
+ import { ChangeEvent, DragEvent, useState } from 'react'
2
+ import { Paper, Typography } from '@mui/material'
3
+ import CloudUploadIcon from '@mui/icons-material/CloudUpload'
4
+ import { CorrectionData } from '../types'
5
+
6
+ interface FileUploadProps {
7
+ onUpload: (data: CorrectionData) => void
8
+ }
9
+
10
+ export default function FileUpload({ onUpload }: FileUploadProps) {
11
+ const [isDragging, setIsDragging] = useState(false)
12
+
13
+ const handleDragOver = (e: DragEvent) => {
14
+ e.preventDefault()
15
+ setIsDragging(true)
16
+ }
17
+
18
+ const handleDragLeave = () => {
19
+ setIsDragging(false)
20
+ }
21
+
22
+ const handleDrop = async (e: DragEvent) => {
23
+ e.preventDefault()
24
+ setIsDragging(false)
25
+ const file = e.dataTransfer.files[0]
26
+ await processFile(file)
27
+ }
28
+
29
+ const handleFileInput = async (e: ChangeEvent<HTMLInputElement>) => {
30
+ if (e.target.files?.length) {
31
+ await processFile(e.target.files[0])
32
+ }
33
+ }
34
+
35
+ const processFile = async (file: File) => {
36
+ try {
37
+ const text = await file.text()
38
+ const data = JSON.parse(text)
39
+ onUpload(data)
40
+ } catch (error) {
41
+ console.error('Error processing file:', error)
42
+ // TODO: Add error handling UI
43
+ }
44
+ }
45
+
46
+ return (
47
+ <Paper
48
+ sx={{
49
+ p: 4,
50
+ display: 'flex',
51
+ flexDirection: 'column',
52
+ alignItems: 'center',
53
+ backgroundColor: isDragging ? 'action.hover' : 'background.paper',
54
+ cursor: 'pointer',
55
+ }}
56
+ onDragOver={handleDragOver}
57
+ onDragLeave={handleDragLeave}
58
+ onDrop={handleDrop}
59
+ onClick={() => document.getElementById('file-input')?.click()}
60
+ >
61
+ <input
62
+ type="file"
63
+ id="file-input"
64
+ style={{ display: 'none' }}
65
+ accept="application/json"
66
+ onChange={handleFileInput}
67
+ />
68
+ <CloudUploadIcon sx={{ fontSize: 48, mb: 2 }} />
69
+ <Typography variant="h6" gutterBottom>
70
+ Upload Lyrics Correction Review JSON
71
+ </Typography>
72
+ <Typography variant="body2" color="text.secondary">
73
+ Drag and drop a file here, or click to select
74
+ </Typography>
75
+ </Paper>
76
+ )
77
+ }