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,281 @@
1
+ import {
2
+ Dialog,
3
+ DialogTitle,
4
+ DialogContent,
5
+ DialogActions,
6
+ Button,
7
+ Box,
8
+ Typography,
9
+ Chip,
10
+ LinearProgress,
11
+ IconButton,
12
+ useMediaQuery,
13
+ useTheme,
14
+ Slide
15
+ } from '@mui/material'
16
+ import { TransitionProps } from '@mui/material/transitions'
17
+ import CloseIcon from '@mui/icons-material/Close'
18
+ import ArrowForwardIcon from '@mui/icons-material/ArrowForward'
19
+ import { forwardRef } from 'react'
20
+
21
+ interface CorrectionDetailCardProps {
22
+ open: boolean
23
+ onClose: () => void
24
+ originalWord: string
25
+ correctedWord: string
26
+ category: string | null
27
+ confidence: number
28
+ reason: string
29
+ handler: string
30
+ source: string
31
+ onRevert: () => void
32
+ onEdit: () => void
33
+ onAccept: () => void
34
+ }
35
+
36
+ // Slide transition for mobile
37
+ const Transition = forwardRef(function Transition(
38
+ props: TransitionProps & {
39
+ children: React.ReactElement<any, any>
40
+ },
41
+ ref: React.Ref<unknown>
42
+ ) {
43
+ return <Slide direction="up" ref={ref} {...props} />
44
+ })
45
+
46
+ // Format category name for display
47
+ const formatCategory = (category: string | null): string => {
48
+ if (!category) return 'Unknown'
49
+ return category
50
+ .split('_')
51
+ .map(word => word.charAt(0) + word.slice(1).toLowerCase())
52
+ .join(' ')
53
+ }
54
+
55
+ // Get emoji/icon for category
56
+ const getCategoryIcon = (category: string | null): string => {
57
+ if (!category) return '📝'
58
+ const icons: Record<string, string> = {
59
+ 'SOUND_ALIKE': '🎵',
60
+ 'PUNCTUATION_ONLY': '✏️',
61
+ 'BACKGROUND_VOCALS': '🎤',
62
+ 'EXTRA_WORDS': '➕',
63
+ 'REPEATED_SECTION': '🔁',
64
+ 'COMPLEX_MULTI_ERROR': '🔧',
65
+ 'AMBIGUOUS': '❓',
66
+ 'NO_ERROR': '✅'
67
+ }
68
+ return icons[category] || '📝'
69
+ }
70
+
71
+ // Get confidence color
72
+ const getConfidenceColor = (confidence: number): 'error' | 'warning' | 'success' => {
73
+ if (confidence < 0.6) return 'error'
74
+ if (confidence < 0.8) return 'warning'
75
+ return 'success'
76
+ }
77
+
78
+ export default function CorrectionDetailCard({
79
+ open,
80
+ onClose,
81
+ originalWord,
82
+ correctedWord,
83
+ category,
84
+ confidence,
85
+ reason,
86
+ handler,
87
+ source,
88
+ onRevert,
89
+ onEdit,
90
+ onAccept
91
+ }: CorrectionDetailCardProps) {
92
+ const theme = useTheme()
93
+ const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
94
+ const fullScreen = isMobile
95
+
96
+ return (
97
+ <Dialog
98
+ open={open}
99
+ onClose={onClose}
100
+ fullScreen={fullScreen}
101
+ maxWidth="sm"
102
+ fullWidth
103
+ TransitionComponent={isMobile ? Transition : undefined}
104
+ PaperProps={{
105
+ sx: {
106
+ ...(isMobile && {
107
+ position: 'fixed',
108
+ bottom: 0,
109
+ m: 0,
110
+ borderRadius: '16px 16px 0 0',
111
+ maxHeight: '85vh'
112
+ })
113
+ }
114
+ }}
115
+ >
116
+ <DialogTitle sx={{ pb: 1 }}>
117
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
118
+ <Typography variant="h6" sx={{ fontSize: '1.1rem', fontWeight: 600 }}>
119
+ Correction Details
120
+ </Typography>
121
+ <IconButton
122
+ aria-label="close"
123
+ onClick={onClose}
124
+ size="small"
125
+ >
126
+ <CloseIcon />
127
+ </IconButton>
128
+ </Box>
129
+ </DialogTitle>
130
+
131
+ <DialogContent>
132
+ {/* Original → Corrected */}
133
+ <Box sx={{ mb: 3 }}>
134
+ <Typography variant="caption" color="text.secondary" sx={{ mb: 0.5, display: 'block' }}>
135
+ Change
136
+ </Typography>
137
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
138
+ <Box
139
+ sx={{
140
+ px: 2,
141
+ py: 1,
142
+ bgcolor: 'error.lighter',
143
+ borderRadius: 1,
144
+ textDecoration: 'line-through',
145
+ flex: 1,
146
+ textAlign: 'center'
147
+ }}
148
+ >
149
+ <Typography variant="body1" sx={{ fontWeight: 500 }}>
150
+ {originalWord}
151
+ </Typography>
152
+ </Box>
153
+ <ArrowForwardIcon color="action" />
154
+ <Box
155
+ sx={{
156
+ px: 2,
157
+ py: 1,
158
+ bgcolor: 'success.lighter',
159
+ borderRadius: 1,
160
+ flex: 1,
161
+ textAlign: 'center'
162
+ }}
163
+ >
164
+ <Typography variant="body1" sx={{ fontWeight: 600 }}>
165
+ {correctedWord}
166
+ </Typography>
167
+ </Box>
168
+ </Box>
169
+ </Box>
170
+
171
+ {/* Category */}
172
+ {category && (
173
+ <Box sx={{ mb: 2 }}>
174
+ <Typography variant="caption" color="text.secondary" sx={{ mb: 0.5, display: 'block' }}>
175
+ Category
176
+ </Typography>
177
+ <Chip
178
+ label={`${getCategoryIcon(category)} ${formatCategory(category)}`}
179
+ size="small"
180
+ variant="outlined"
181
+ sx={{ fontSize: '0.875rem' }}
182
+ />
183
+ </Box>
184
+ )}
185
+
186
+ {/* Confidence */}
187
+ <Box sx={{ mb: 2 }}>
188
+ <Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
189
+ <Typography variant="caption" color="text.secondary">
190
+ Confidence
191
+ </Typography>
192
+ <Typography variant="caption" sx={{ fontWeight: 600 }}>
193
+ {(confidence * 100).toFixed(0)}%
194
+ </Typography>
195
+ </Box>
196
+ <LinearProgress
197
+ variant="determinate"
198
+ value={confidence * 100}
199
+ color={getConfidenceColor(confidence)}
200
+ sx={{ height: 8, borderRadius: 1 }}
201
+ />
202
+ </Box>
203
+
204
+ {/* Reasoning */}
205
+ <Box sx={{ mb: 2 }}>
206
+ <Typography variant="caption" color="text.secondary" sx={{ mb: 0.5, display: 'block' }}>
207
+ Reasoning
208
+ </Typography>
209
+ <Typography
210
+ variant="body2"
211
+ sx={{
212
+ p: 1.5,
213
+ bgcolor: 'grey.50',
214
+ borderRadius: 1,
215
+ border: '1px solid',
216
+ borderColor: 'grey.200',
217
+ lineHeight: 1.6
218
+ }}
219
+ >
220
+ {reason}
221
+ </Typography>
222
+ </Box>
223
+
224
+ {/* Metadata */}
225
+ <Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
226
+ <Chip
227
+ label={`Handler: ${handler}`}
228
+ size="small"
229
+ variant="outlined"
230
+ sx={{ fontSize: '0.7rem' }}
231
+ />
232
+ <Chip
233
+ label={`Source: ${source}`}
234
+ size="small"
235
+ variant="outlined"
236
+ sx={{ fontSize: '0.7rem' }}
237
+ />
238
+ </Box>
239
+ </DialogContent>
240
+
241
+ <DialogActions sx={{ p: 2, gap: 1, flexDirection: isMobile ? 'column' : 'row' }}>
242
+ <Button
243
+ onClick={() => {
244
+ onRevert()
245
+ onClose()
246
+ }}
247
+ variant="outlined"
248
+ color="error"
249
+ fullWidth={isMobile}
250
+ sx={{ minHeight: isMobile ? '44px' : '36px' }}
251
+ >
252
+ Revert to Original
253
+ </Button>
254
+ <Button
255
+ onClick={() => {
256
+ onEdit()
257
+ onClose()
258
+ }}
259
+ variant="outlined"
260
+ fullWidth={isMobile}
261
+ sx={{ minHeight: isMobile ? '44px' : '36px' }}
262
+ >
263
+ Edit Correction
264
+ </Button>
265
+ <Button
266
+ onClick={() => {
267
+ onAccept()
268
+ onClose()
269
+ }}
270
+ variant="contained"
271
+ color="success"
272
+ fullWidth={isMobile}
273
+ sx={{ minHeight: isMobile ? '44px' : '36px' }}
274
+ >
275
+ Mark as Correct
276
+ </Button>
277
+ </DialogActions>
278
+ </Dialog>
279
+ )
280
+ }
281
+
@@ -0,0 +1,162 @@
1
+ import { Paper, Box, Typography } from '@mui/material'
2
+ import { COLORS } from './shared/constants'
3
+
4
+ interface MetricProps {
5
+ color?: string
6
+ label: string
7
+ value: string | number
8
+ description: string
9
+ details?: Array<{ label: string, value: string | number }>
10
+ onClick?: () => void
11
+ }
12
+
13
+ function Metric({ color, label, value, description, details, onClick }: MetricProps) {
14
+ return (
15
+ <Paper
16
+ sx={{
17
+ p: 0.8,
18
+ pt: 0,
19
+ height: '100%',
20
+ cursor: onClick ? 'pointer' : 'default',
21
+ '&:hover': onClick ? {
22
+ bgcolor: 'action.hover'
23
+ } : undefined,
24
+ display: 'flex',
25
+ flexDirection: 'column'
26
+ }}
27
+ onClick={onClick}
28
+ >
29
+ <Box sx={{ display: 'flex', alignItems: 'center', mb: 0.5, mt: 0.8 }}>
30
+ {color && (
31
+ <Box
32
+ sx={{
33
+ width: 12,
34
+ height: 12,
35
+ borderRadius: 1,
36
+ bgcolor: color,
37
+ mr: 0.5,
38
+ }}
39
+ />
40
+ )}
41
+ <Typography variant="subtitle2" color="text.secondary" sx={{ fontSize: '0.7rem' }}>
42
+ {label}
43
+ </Typography>
44
+ </Box>
45
+ <Box sx={{ display: 'flex', alignItems: 'baseline', gap: 0.5, mb: 0.3 }}>
46
+ <Typography variant="h6" sx={{ fontSize: '1.1rem' }}>
47
+ {value}
48
+ </Typography>
49
+ </Box>
50
+ <Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem' }}>
51
+ {description}
52
+ </Typography>
53
+ {details && (
54
+ <Box sx={{ mt: 0.5, flex: 1, overflow: 'auto' }}>
55
+ {details.map((detail, index) => (
56
+ <Box key={index} sx={{ display: 'flex', justifyContent: 'space-between', mt: 0.3 }}>
57
+ <Typography variant="caption" color="text.secondary" sx={{ fontSize: '0.7rem' }}>
58
+ {detail.label}
59
+ </Typography>
60
+ <Typography variant="caption" sx={{ fontSize: '0.7rem' }}>
61
+ {detail.value}
62
+ </Typography>
63
+ </Box>
64
+ ))}
65
+ </Box>
66
+ )}
67
+ </Paper>
68
+ )
69
+ }
70
+
71
+ interface CorrectionMetricsProps {
72
+ // Anchor metrics
73
+ anchorCount?: number
74
+ multiSourceAnchors?: number
75
+ anchorWordCount?: number
76
+ // Gap metrics
77
+ correctedGapCount?: number
78
+ uncorrectedGapCount?: number
79
+ uncorrectedGaps?: Array<{
80
+ position: string
81
+ length: number
82
+ }>
83
+ // Correction details
84
+ replacedCount?: number
85
+ addedCount?: number
86
+ deletedCount?: number
87
+ // Add total words count
88
+ totalWords?: number
89
+ onMetricClick?: {
90
+ anchor?: () => void
91
+ corrected?: () => void
92
+ uncorrected?: () => void
93
+ }
94
+ }
95
+
96
+ export default function CorrectionMetrics({
97
+ anchorCount,
98
+ multiSourceAnchors = 0,
99
+ anchorWordCount = 0,
100
+ correctedGapCount = 0,
101
+ uncorrectedGapCount = 0,
102
+ uncorrectedGaps = [],
103
+ replacedCount = 0,
104
+ addedCount = 0,
105
+ deletedCount = 0,
106
+ totalWords = 0,
107
+ onMetricClick
108
+ }: CorrectionMetricsProps) {
109
+ // Calculate percentages based on word counts
110
+ const anchorPercentage = totalWords > 0 ? Math.round((anchorWordCount / totalWords) * 100) : 0
111
+ const uncorrectedWordCount = uncorrectedGaps?.reduce((sum, gap) => sum + gap.length, 0) ?? 0
112
+ const uncorrectedPercentage = totalWords > 0 ? Math.round((uncorrectedWordCount / totalWords) * 100) : 0
113
+ const correctedWordCount = replacedCount + addedCount
114
+ const correctedPercentage = totalWords > 0 ?
115
+ Math.round((correctedWordCount / totalWords) * 100) : 0
116
+
117
+ return (
118
+ <Box sx={{ height: '100%', display: 'flex' }}>
119
+ <Box sx={{ flex: 1, display: 'flex', flexDirection: 'row', gap: 1, height: '100%' }}>
120
+ <Box sx={{ flex: 1, height: '100%' }}>
121
+ <Metric
122
+ color={COLORS.anchor}
123
+ label="Anchor Sequences"
124
+ value={`${anchorCount ?? '-'} (${anchorPercentage}%)`}
125
+ description="Matched sections between transcription and reference"
126
+ details={[
127
+ { label: 'Words in Anchors', value: anchorWordCount },
128
+ { label: 'Multi-source Matches', value: multiSourceAnchors }
129
+ ]}
130
+ onClick={onMetricClick?.anchor}
131
+ />
132
+ </Box>
133
+ <Box sx={{ flex: 1, height: '100%' }}>
134
+ <Metric
135
+ color={COLORS.corrected}
136
+ label="Corrected Gaps"
137
+ value={`${correctedGapCount} (${correctedPercentage}%)`}
138
+ description="Successfully corrected sections"
139
+ details={[
140
+ { label: 'Words Replaced', value: replacedCount },
141
+ { label: 'Words Added / Deleted', value: `+${addedCount} / -${deletedCount}` }
142
+ ]}
143
+ onClick={onMetricClick?.corrected}
144
+ />
145
+ </Box>
146
+ <Box sx={{ flex: 1, height: '100%' }}>
147
+ <Metric
148
+ color={COLORS.uncorrectedGap}
149
+ label="Uncorrected Gaps"
150
+ value={`${uncorrectedGapCount} (${uncorrectedPercentage}%)`}
151
+ description="Sections that may need manual review"
152
+ details={[
153
+ { label: 'Words Uncorrected', value: uncorrectedWordCount },
154
+ { label: 'Number of Gaps', value: uncorrectedGaps.length }
155
+ ]}
156
+ onClick={onMetricClick?.uncorrected}
157
+ />
158
+ </Box>
159
+ </Box>
160
+ </Box>
161
+ )
162
+ }
@@ -0,0 +1,257 @@
1
+ import { Box, Typography, styled } from '@mui/material'
2
+ import { LyricsSegment, WordCorrection, AnchorSequence, GapSequence } from '../types'
3
+ import { COLORS } from './shared/constants'
4
+ import WarningIcon from '@mui/icons-material/Warning'
5
+
6
+ interface DurationTimelineViewProps {
7
+ segments: LyricsSegment[]
8
+ corrections: WordCorrection[]
9
+ anchors: AnchorSequence[]
10
+ gaps: GapSequence[]
11
+ onWordClick?: (wordId: string) => void
12
+ }
13
+
14
+ const TimelineContainer = styled(Box)({
15
+ display: 'flex',
16
+ flexDirection: 'column',
17
+ gap: '12px',
18
+ padding: '8px',
19
+ overflowX: 'auto',
20
+ minWidth: '100%'
21
+ })
22
+
23
+ const SegmentTimeline = styled(Box)({
24
+ display: 'flex',
25
+ flexDirection: 'column',
26
+ gap: '4px',
27
+ minWidth: '100%'
28
+ })
29
+
30
+ const TimelineRuler = styled(Box)({
31
+ position: 'relative',
32
+ height: '20px',
33
+ borderBottom: '1px solid #ccc',
34
+ marginBottom: '4px'
35
+ })
36
+
37
+ const TimelineMark = styled(Box)({
38
+ position: 'absolute',
39
+ width: '1px',
40
+ height: '8px',
41
+ backgroundColor: '#999',
42
+ bottom: 0
43
+ })
44
+
45
+ const TimelineLabel = styled(Typography)({
46
+ position: 'absolute',
47
+ fontSize: '0.65rem',
48
+ color: '#666',
49
+ bottom: '10px',
50
+ transform: 'translateX(-50%)',
51
+ whiteSpace: 'nowrap'
52
+ })
53
+
54
+ const WordsBar = styled(Box)({
55
+ position: 'relative',
56
+ height: '60px', // Increased from 48px for better readability
57
+ display: 'flex',
58
+ alignItems: 'stretch',
59
+ minWidth: '100%',
60
+ touchAction: 'pan-y', // Better mobile scrolling
61
+ backgroundColor: '#f5f5f5',
62
+ borderRadius: '4px',
63
+ marginBottom: '8px'
64
+ })
65
+
66
+ const WordBar = styled(Box, {
67
+ shouldForwardProp: (prop) => !['isLong', 'isCorrected', 'isGap', 'isAnchor'].includes(prop as string)
68
+ })<{ isLong?: boolean; isCorrected?: boolean; isGap?: boolean; isAnchor?: boolean }>(
69
+ ({ isLong, isCorrected, isGap, isAnchor }) => ({
70
+ position: 'absolute',
71
+ height: '100%',
72
+ display: 'flex',
73
+ flexDirection: 'column',
74
+ justifyContent: 'center',
75
+ alignItems: 'center',
76
+ padding: '2px 4px',
77
+ borderRight: '1px solid rgba(255,255,255,0.3)',
78
+ fontSize: '0.75rem',
79
+ cursor: 'pointer',
80
+ transition: 'all 0.2s',
81
+ backgroundColor: isAnchor
82
+ ? COLORS.anchor
83
+ : isCorrected
84
+ ? COLORS.corrected
85
+ : isGap
86
+ ? COLORS.uncorrectedGap
87
+ : '#e0e0e0',
88
+ border: isLong ? '2px solid #f44336' : 'none',
89
+ boxShadow: isLong ? '0 0 4px rgba(244, 67, 54, 0.5)' : 'none',
90
+ '&:hover': {
91
+ opacity: 0.8,
92
+ transform: 'scale(1.02)',
93
+ zIndex: 10
94
+ },
95
+ overflow: 'hidden',
96
+ textOverflow: 'ellipsis',
97
+ whiteSpace: 'nowrap',
98
+ minWidth: '20px' // Ensure very short words are still tappable
99
+ })
100
+ )
101
+
102
+ const OriginalWordLabel = styled(Typography)({
103
+ fontSize: '0.65rem',
104
+ color: '#888',
105
+ lineHeight: 1.1,
106
+ marginBottom: '3px',
107
+ textDecoration: 'line-through',
108
+ opacity: 0.85,
109
+ fontWeight: 500,
110
+ backgroundColor: 'rgba(255, 255, 255, 0.8)',
111
+ padding: '1px 3px',
112
+ borderRadius: '2px'
113
+ })
114
+
115
+ const LongWordWarning = styled(WarningIcon)({
116
+ position: 'absolute',
117
+ top: '-2px',
118
+ right: '-2px',
119
+ fontSize: '16px',
120
+ color: '#f44336'
121
+ })
122
+
123
+ export default function DurationTimelineView({
124
+ segments,
125
+ corrections,
126
+ anchors,
127
+ gaps,
128
+ onWordClick
129
+ }: DurationTimelineViewProps) {
130
+
131
+ const timeToPosition = (time: number, startTime: number, endTime: number): number => {
132
+ const duration = endTime - startTime
133
+ if (duration === 0) return 0
134
+ const position = ((time - startTime) / duration) * 100
135
+ return Math.max(0, Math.min(100, position))
136
+ }
137
+
138
+ const generateTimelineMarks = (startTime: number, endTime: number) => {
139
+ const marks = []
140
+ const startSecond = Math.floor(startTime)
141
+ const endSecond = Math.ceil(endTime)
142
+
143
+ // Generate marks roughly every 2 seconds for readability
144
+ for (let time = startSecond; time <= endSecond; time += 2) {
145
+ if (time >= startTime && time <= endTime) {
146
+ const position = timeToPosition(time, startTime, endTime)
147
+ marks.push(
148
+ <Box key={time}>
149
+ <TimelineMark sx={{ left: `${position}%` }} />
150
+ <TimelineLabel sx={{ left: `${position}%` }}>
151
+ {time}s
152
+ </TimelineLabel>
153
+ </Box>
154
+ )
155
+ }
156
+ }
157
+ return marks
158
+ }
159
+
160
+ return (
161
+ <TimelineContainer>
162
+ {segments.map((segment) => {
163
+ // Calculate segment time range
164
+ const segmentWords = segment.words.filter(w => w.start_time !== null && w.end_time !== null)
165
+ if (segmentWords.length === 0) return null
166
+
167
+ const startTime = Math.min(...segmentWords.map(w => w.start_time!))
168
+ const endTime = Math.max(...segmentWords.map(w => w.end_time!))
169
+
170
+ return (
171
+ <SegmentTimeline key={segment.id}>
172
+ <TimelineRuler>
173
+ {generateTimelineMarks(startTime, endTime)}
174
+ </TimelineRuler>
175
+ <WordsBar>
176
+ {segment.words.map((word, wordIndex) => {
177
+ if (word.start_time === null || word.end_time === null) return null
178
+
179
+ const leftPosition = timeToPosition(word.start_time, startTime, endTime)
180
+ const rightPosition = timeToPosition(word.end_time, startTime, endTime)
181
+ const width = rightPosition - leftPosition
182
+ const duration = word.end_time - word.start_time
183
+ const isLong = duration > 2.0
184
+
185
+ // Check if this word is corrected
186
+ const correction = corrections.find(c =>
187
+ c.corrected_word_id === word.id || c.word_id === word.id
188
+ )
189
+
190
+ // Check if anchor
191
+ const isAnchor = anchors.some(a =>
192
+ a.transcribed_word_ids.includes(word.id)
193
+ )
194
+
195
+ // Check if uncorrected gap
196
+ const isGap = gaps.some(g =>
197
+ g.transcribed_word_ids.includes(word.id)
198
+ ) && !correction
199
+
200
+ return (
201
+ <WordBar
202
+ key={`${segment.id}-${wordIndex}`}
203
+ sx={{
204
+ left: `${leftPosition}%`,
205
+ width: `${width}%`
206
+ }}
207
+ isLong={isLong}
208
+ isCorrected={!!correction}
209
+ isGap={isGap}
210
+ isAnchor={isAnchor}
211
+ onClick={() => onWordClick?.(word.id)}
212
+ >
213
+ {isLong && <LongWordWarning />}
214
+ {correction && (
215
+ <OriginalWordLabel>
216
+ {correction.original_word}
217
+ </OriginalWordLabel>
218
+ )}
219
+ <Typography
220
+ sx={{
221
+ fontSize: correction ? '0.85rem' : '0.75rem',
222
+ fontWeight: correction ? 700 : 500,
223
+ lineHeight: 1.2,
224
+ overflow: 'hidden',
225
+ textOverflow: 'ellipsis',
226
+ whiteSpace: 'nowrap',
227
+ width: '100%',
228
+ textAlign: 'center',
229
+ color: correction ? '#1b5e20' : 'inherit'
230
+ }}
231
+ >
232
+ {word.text}
233
+ </Typography>
234
+ {duration > 0.1 && (
235
+ <Typography
236
+ sx={{
237
+ fontSize: '0.6rem',
238
+ color: 'rgba(0,0,0,0.6)',
239
+ lineHeight: 1,
240
+ marginTop: '3px',
241
+ fontWeight: 600
242
+ }}
243
+ >
244
+ {duration.toFixed(1)}s
245
+ </Typography>
246
+ )}
247
+ </WordBar>
248
+ )
249
+ })}
250
+ </WordsBar>
251
+ </SegmentTimeline>
252
+ )
253
+ })}
254
+ </TimelineContainer>
255
+ )
256
+ }
257
+