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,180 @@
1
+ import { Box, IconButton, Slider, Typography } from '@mui/material'
2
+ import PlayArrowIcon from '@mui/icons-material/PlayArrow'
3
+ import PauseIcon from '@mui/icons-material/Pause'
4
+ import { useEffect, useRef, useState, useCallback } from 'react'
5
+ import { ApiClient } from '../api'
6
+
7
+ interface AudioPlayerProps {
8
+ apiClient: ApiClient | null,
9
+ onTimeUpdate?: (time: number) => void,
10
+ audioHash: string
11
+ }
12
+
13
+ export default function AudioPlayer({ apiClient, onTimeUpdate, audioHash }: AudioPlayerProps) {
14
+ const [isPlaying, setIsPlaying] = useState(false)
15
+ const [currentTime, setCurrentTime] = useState(0)
16
+ const [duration, setDuration] = useState(0)
17
+ const audioRef = useRef<HTMLAudioElement | null>(null)
18
+
19
+ useEffect(() => {
20
+ if (!apiClient) return
21
+
22
+ const audio = new Audio(apiClient.getAudioUrl(audioHash))
23
+ audioRef.current = audio
24
+
25
+ // Add requestAnimationFrame for smoother updates
26
+ let animationFrameId: number
27
+
28
+ const updateTime = () => {
29
+ const time = audio.currentTime
30
+ setCurrentTime(time)
31
+ onTimeUpdate?.(time)
32
+ animationFrameId = requestAnimationFrame(updateTime)
33
+ }
34
+
35
+ audio.addEventListener('play', () => {
36
+ setIsPlaying(true)
37
+ window.isAudioPlaying = true
38
+ updateTime()
39
+ })
40
+
41
+ audio.addEventListener('pause', () => {
42
+ setIsPlaying(false)
43
+ window.isAudioPlaying = false
44
+ cancelAnimationFrame(animationFrameId)
45
+ })
46
+
47
+ audio.addEventListener('ended', () => {
48
+ cancelAnimationFrame(animationFrameId)
49
+ setIsPlaying(false)
50
+ window.isAudioPlaying = false
51
+ setCurrentTime(0)
52
+ })
53
+
54
+ audio.addEventListener('loadedmetadata', () => {
55
+ setDuration(audio.duration)
56
+ })
57
+
58
+ return () => {
59
+ cancelAnimationFrame(animationFrameId)
60
+ audio.pause()
61
+ audio.src = ''
62
+ audioRef.current = null
63
+ window.isAudioPlaying = false
64
+ }
65
+ }, [apiClient, onTimeUpdate, audioHash])
66
+
67
+ const handlePlayPause = () => {
68
+ if (!audioRef.current) return
69
+
70
+ if (isPlaying) {
71
+ audioRef.current.pause()
72
+ } else {
73
+ audioRef.current.play()
74
+ }
75
+ setIsPlaying(!isPlaying)
76
+ }
77
+
78
+ const handleSeek = (_: Event, newValue: number | number[]) => {
79
+ if (!audioRef.current) return
80
+ const time = newValue as number
81
+ audioRef.current.currentTime = time
82
+ setCurrentTime(time)
83
+ }
84
+
85
+ const formatTime = (seconds: number) => {
86
+ const mins = Math.floor(seconds / 60)
87
+ const secs = Math.floor(seconds % 60)
88
+ return `${mins}:${secs.toString().padStart(2, '0')}`
89
+ }
90
+
91
+ // Add this method to expose seeking functionality
92
+ const seekAndPlay = (time: number) => {
93
+ if (!audioRef.current) return
94
+
95
+ audioRef.current.currentTime = time
96
+ setCurrentTime(time)
97
+ audioRef.current.play()
98
+ setIsPlaying(true)
99
+ }
100
+
101
+ const togglePlayback = useCallback(() => {
102
+ if (!audioRef.current) return
103
+
104
+ if (isPlaying) {
105
+ audioRef.current.pause()
106
+ } else {
107
+ audioRef.current.play()
108
+ }
109
+ setIsPlaying(!isPlaying)
110
+ }, [isPlaying])
111
+
112
+ // Expose methods and duration globally
113
+ useEffect(() => {
114
+ if (!apiClient) return
115
+
116
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
117
+ const win = window as any
118
+ win.seekAndPlayAudio = seekAndPlay
119
+ win.toggleAudioPlayback = togglePlayback
120
+ win.getAudioDuration = () => duration
121
+
122
+ return () => {
123
+ delete win.seekAndPlayAudio
124
+ delete win.toggleAudioPlayback
125
+ delete win.getAudioDuration
126
+ }
127
+ }, [apiClient, togglePlayback, duration])
128
+
129
+ if (!apiClient) return null
130
+
131
+ return (
132
+ <Box sx={{
133
+ display: 'flex',
134
+ alignItems: 'center',
135
+ gap: 0.5,
136
+ backgroundColor: 'background.paper',
137
+ borderRadius: 1,
138
+ height: '32px',
139
+ }}>
140
+ <Typography variant="body2" color="text.secondary" sx={{ mr: 0.5, fontSize: '0.75rem' }}>
141
+ Playback:
142
+ </Typography>
143
+
144
+ <IconButton
145
+ onClick={handlePlayPause}
146
+ size="small"
147
+ sx={{ p: 0.5 }}
148
+ >
149
+ {isPlaying ? <PauseIcon fontSize="small" /> : <PlayArrowIcon fontSize="small" />}
150
+ </IconButton>
151
+
152
+ <Typography variant="body2" sx={{ minWidth: 32, fontSize: '0.75rem' }}>
153
+ {formatTime(currentTime)}
154
+ </Typography>
155
+
156
+ <Slider
157
+ value={currentTime}
158
+ min={0}
159
+ max={duration}
160
+ onChange={handleSeek}
161
+ size="small"
162
+ sx={{
163
+ width: 150,
164
+ mx: 0.5,
165
+ '& .MuiSlider-thumb': {
166
+ width: 10,
167
+ height: 10,
168
+ },
169
+ '& .MuiSlider-rail, & .MuiSlider-track': {
170
+ height: 3
171
+ }
172
+ }}
173
+ />
174
+
175
+ <Typography variant="body2" sx={{ minWidth: 32, fontSize: '0.75rem' }}>
176
+ {formatTime(duration)}
177
+ </Typography>
178
+ </Box>
179
+ )
180
+ }
@@ -0,0 +1,167 @@
1
+ import { Box, IconButton, Tooltip, useMediaQuery, useTheme } from '@mui/material'
2
+ import UndoIcon from '@mui/icons-material/Undo'
3
+ import EditIcon from '@mui/icons-material/Edit'
4
+ import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'
5
+ import { COLORS } from './shared/constants'
6
+ import { styled } from '@mui/material/styles'
7
+
8
+ interface CorrectionInfo {
9
+ originalWord: string
10
+ handler: string
11
+ confidence: number
12
+ source: string
13
+ reason?: string
14
+ }
15
+
16
+ interface CorrectedWordWithActionsProps {
17
+ word: string
18
+ originalWord: string
19
+ correction: CorrectionInfo
20
+ onRevert: () => void
21
+ onEdit: () => void
22
+ onAccept: () => void
23
+ onClick?: () => void
24
+ backgroundColor?: string
25
+ shouldFlash?: boolean
26
+ }
27
+
28
+ const WordContainer = styled(Box, {
29
+ shouldForwardProp: (prop) => !['shouldFlash'].includes(prop as string)
30
+ })<{ shouldFlash?: boolean }>(({ shouldFlash }) => ({
31
+ display: 'inline-flex',
32
+ alignItems: 'center',
33
+ gap: '2px',
34
+ padding: '1px 3px',
35
+ borderRadius: '2px',
36
+ cursor: 'pointer',
37
+ position: 'relative',
38
+ backgroundColor: COLORS.corrected,
39
+ animation: shouldFlash ? 'flash 1s ease-in-out infinite' : 'none',
40
+ '@keyframes flash': {
41
+ '0%, 100%': { opacity: 1 },
42
+ '50%': { opacity: 0.5 }
43
+ },
44
+ '&:hover': {
45
+ backgroundColor: '#c8e6c9'
46
+ }
47
+ }))
48
+
49
+ const OriginalWordLabel = styled(Box)({
50
+ position: 'absolute',
51
+ top: '-14px',
52
+ left: '0',
53
+ fontSize: '0.6rem',
54
+ color: '#666',
55
+ textDecoration: 'line-through',
56
+ opacity: 0.7,
57
+ whiteSpace: 'nowrap',
58
+ pointerEvents: 'none'
59
+ })
60
+
61
+ const ActionsContainer = styled(Box)({
62
+ display: 'inline-flex',
63
+ alignItems: 'center',
64
+ gap: '1px',
65
+ marginLeft: '2px'
66
+ })
67
+
68
+ const ActionButton = styled(IconButton)(({ theme }) => ({
69
+ padding: '2px',
70
+ minWidth: '20px',
71
+ minHeight: '20px',
72
+ width: '20px',
73
+ height: '20px',
74
+ backgroundColor: 'rgba(255, 255, 255, 0.9)',
75
+ border: '1px solid rgba(0, 0, 0, 0.1)',
76
+ '&:hover': {
77
+ backgroundColor: 'rgba(255, 255, 255, 1)',
78
+ transform: 'scale(1.1)'
79
+ },
80
+ '& .MuiSvgIcon-root': {
81
+ fontSize: '0.875rem'
82
+ },
83
+ // Ensure minimum touch target on mobile
84
+ [theme.breakpoints.down('sm')]: {
85
+ minWidth: '28px',
86
+ minHeight: '28px',
87
+ width: '28px',
88
+ height: '28px',
89
+ padding: '4px'
90
+ }
91
+ }))
92
+
93
+ export default function CorrectedWordWithActions({
94
+ word,
95
+ originalWord,
96
+ onRevert,
97
+ onEdit,
98
+ onAccept,
99
+ onClick,
100
+ backgroundColor,
101
+ shouldFlash
102
+ }: CorrectedWordWithActionsProps) {
103
+ const theme = useTheme()
104
+ const isMobile = useMediaQuery(theme.breakpoints.down('sm'))
105
+
106
+ const handleAction = (e: React.MouseEvent, action: () => void) => {
107
+ e.stopPropagation()
108
+ action()
109
+ }
110
+
111
+ return (
112
+ <WordContainer
113
+ shouldFlash={shouldFlash}
114
+ sx={{ backgroundColor: backgroundColor || COLORS.corrected }}
115
+ onClick={onClick}
116
+ >
117
+ <OriginalWordLabel>{originalWord}</OriginalWordLabel>
118
+
119
+ <Box
120
+ component="span"
121
+ sx={{
122
+ fontSize: '0.85rem',
123
+ lineHeight: 1.2,
124
+ fontWeight: 600
125
+ }}
126
+ >
127
+ {word}
128
+ </Box>
129
+
130
+ <ActionsContainer>
131
+ <Tooltip title="Revert to original" placement="top" arrow>
132
+ <ActionButton
133
+ size="small"
134
+ onClick={(e) => handleAction(e, onRevert)}
135
+ aria-label="revert correction"
136
+ >
137
+ <UndoIcon />
138
+ </ActionButton>
139
+ </Tooltip>
140
+
141
+ <Tooltip title="Edit correction" placement="top" arrow>
142
+ <ActionButton
143
+ size="small"
144
+ onClick={(e) => handleAction(e, onEdit)}
145
+ aria-label="edit correction"
146
+ >
147
+ <EditIcon />
148
+ </ActionButton>
149
+ </Tooltip>
150
+
151
+ {!isMobile && (
152
+ <Tooltip title="Accept correction" placement="top" arrow>
153
+ <ActionButton
154
+ size="small"
155
+ onClick={(e) => handleAction(e, onAccept)}
156
+ aria-label="accept correction"
157
+ sx={{ color: 'success.main' }}
158
+ >
159
+ <CheckCircleOutlineIcon />
160
+ </ActionButton>
161
+ </Tooltip>
162
+ )}
163
+ </ActionsContainer>
164
+ </WordContainer>
165
+ )
166
+ }
167
+
@@ -0,0 +1,359 @@
1
+ import { useState, useEffect } from 'react'
2
+ import {
3
+ Dialog,
4
+ DialogTitle,
5
+ DialogContent,
6
+ DialogActions,
7
+ Button,
8
+ FormControl,
9
+ InputLabel,
10
+ Select,
11
+ MenuItem,
12
+ TextField,
13
+ Slider,
14
+ Typography,
15
+ Box,
16
+ Paper,
17
+ Chip,
18
+ Alert,
19
+ SelectChangeEvent
20
+ } from '@mui/material'
21
+ import { CorrectionAnnotation, CorrectionAnnotationType, CorrectionAction } from '../types'
22
+
23
+ interface CorrectionAnnotationModalProps {
24
+ open: boolean
25
+ onClose: () => void
26
+ onSave: (annotation: Omit<CorrectionAnnotation, 'annotation_id' | 'timestamp'>) => void
27
+ onSkip: () => void
28
+ // Context about the correction being made
29
+ originalText: string
30
+ correctedText: string
31
+ wordIdsAffected: string[]
32
+ // Optional: What the AI suggested
33
+ agenticProposal?: {
34
+ action: string
35
+ replacement_text?: string
36
+ confidence: number
37
+ reason: string
38
+ gap_category?: string
39
+ }
40
+ // Reference lyrics that were consulted
41
+ referenceSources: string[]
42
+ // Song metadata
43
+ audioHash: string
44
+ artist: string
45
+ title: string
46
+ sessionId: string
47
+ gapId?: string
48
+ }
49
+
50
+ const ANNOTATION_TYPES: { value: CorrectionAnnotationType; label: string; description: string }[] = [
51
+ {
52
+ value: 'SOUND_ALIKE' as CorrectionAnnotationType,
53
+ label: 'Sound-Alike Error',
54
+ description: 'Homophones or similar-sounding words (e.g., "out" vs "now")'
55
+ },
56
+ {
57
+ value: 'BACKGROUND_VOCALS' as CorrectionAnnotationType,
58
+ label: 'Background Vocals',
59
+ description: 'Backing vocals that should be removed from karaoke'
60
+ },
61
+ {
62
+ value: 'EXTRA_WORDS' as CorrectionAnnotationType,
63
+ label: 'Extra Filler Words',
64
+ description: 'Transcription added words like "And", "But", "Well"'
65
+ },
66
+ {
67
+ value: 'PUNCTUATION_ONLY' as CorrectionAnnotationType,
68
+ label: 'Punctuation/Style Only',
69
+ description: 'Only punctuation or capitalization differences'
70
+ },
71
+ {
72
+ value: 'REPEATED_SECTION' as CorrectionAnnotationType,
73
+ label: 'Repeated Section',
74
+ description: 'Chorus or verse repetition not in condensed references'
75
+ },
76
+ {
77
+ value: 'COMPLEX_MULTI_ERROR' as CorrectionAnnotationType,
78
+ label: 'Complex Multi-Error',
79
+ description: 'Multiple different error types in one section'
80
+ },
81
+ {
82
+ value: 'AMBIGUOUS' as CorrectionAnnotationType,
83
+ label: 'Ambiguous',
84
+ description: 'Unclear without listening to audio'
85
+ },
86
+ {
87
+ value: 'NO_ERROR' as CorrectionAnnotationType,
88
+ label: 'No Error',
89
+ description: 'Transcription matches at least one reference source'
90
+ },
91
+ {
92
+ value: 'MANUAL_EDIT' as CorrectionAnnotationType,
93
+ label: 'Manual Edit',
94
+ description: 'Human-initiated correction not from detected gap'
95
+ }
96
+ ]
97
+
98
+ const CONFIDENCE_LABELS: { [key: number]: string } = {
99
+ 1: '1 - Very Uncertain',
100
+ 2: '2 - Somewhat Uncertain',
101
+ 3: '3 - Neutral',
102
+ 4: '4 - Fairly Confident',
103
+ 5: '5 - Very Confident'
104
+ }
105
+
106
+ export default function CorrectionAnnotationModal({
107
+ open,
108
+ onClose,
109
+ onSave,
110
+ onSkip,
111
+ originalText,
112
+ correctedText,
113
+ wordIdsAffected,
114
+ agenticProposal,
115
+ referenceSources,
116
+ audioHash,
117
+ artist,
118
+ title,
119
+ sessionId,
120
+ gapId
121
+ }: CorrectionAnnotationModalProps) {
122
+ const [annotationType, setAnnotationType] = useState<CorrectionAnnotationType>('MANUAL_EDIT' as CorrectionAnnotationType)
123
+ const [actionTaken, setActionTaken] = useState<CorrectionAction>('REPLACE' as CorrectionAction)
124
+ const [confidence, setConfidence] = useState<number>(3)
125
+ const [reasoning, setReasoning] = useState<string>('')
126
+ const [agenticAgreed, setAgenticAgreed] = useState<boolean>(false)
127
+ const [error, setError] = useState<string>('')
128
+
129
+ // Pre-fill if we have an agentic proposal
130
+ useEffect(() => {
131
+ if (agenticProposal && agenticProposal.gap_category) {
132
+ setAnnotationType(agenticProposal.gap_category as CorrectionAnnotationType)
133
+ // Check if human correction matches AI suggestion
134
+ const agreed = agenticProposal.replacement_text
135
+ ? correctedText.toLowerCase().includes(agenticProposal.replacement_text.toLowerCase())
136
+ : false
137
+ setAgenticAgreed(agreed)
138
+ }
139
+ }, [agenticProposal, correctedText])
140
+
141
+ // Determine action type based on correction
142
+ useEffect(() => {
143
+ if (originalText === correctedText) {
144
+ setActionTaken('NO_ACTION' as CorrectionAction)
145
+ } else if (correctedText === '') {
146
+ setActionTaken('DELETE' as CorrectionAction)
147
+ } else if (originalText === '') {
148
+ setActionTaken('INSERT' as CorrectionAction)
149
+ } else {
150
+ setActionTaken('REPLACE' as CorrectionAction)
151
+ }
152
+ }, [originalText, correctedText])
153
+
154
+ const handleSave = () => {
155
+ // Validate
156
+ if (reasoning.length < 10) {
157
+ setError('Reasoning must be at least 10 characters')
158
+ return
159
+ }
160
+
161
+ const annotation: Omit<CorrectionAnnotation, 'annotation_id' | 'timestamp'> = {
162
+ audio_hash: audioHash,
163
+ gap_id: gapId || null,
164
+ annotation_type: annotationType,
165
+ action_taken: actionTaken,
166
+ original_text: originalText,
167
+ corrected_text: correctedText,
168
+ confidence,
169
+ reasoning,
170
+ word_ids_affected: wordIdsAffected,
171
+ agentic_proposal: agenticProposal || null,
172
+ agentic_category: agenticProposal?.gap_category || null,
173
+ agentic_agreed: agenticAgreed,
174
+ reference_sources_consulted: referenceSources,
175
+ artist,
176
+ title,
177
+ session_id: sessionId
178
+ }
179
+
180
+ onSave(annotation)
181
+ handleClose()
182
+ }
183
+
184
+ const handleClose = () => {
185
+ // Reset form
186
+ setAnnotationType('MANUAL_EDIT' as CorrectionAnnotationType)
187
+ setConfidence(3)
188
+ setReasoning('')
189
+ setError('')
190
+ setAgenticAgreed(false)
191
+ onClose()
192
+ }
193
+
194
+ const handleSkipAndClose = () => {
195
+ handleClose()
196
+ onSkip()
197
+ }
198
+
199
+ return (
200
+ <Dialog
201
+ open={open}
202
+ onClose={handleClose}
203
+ maxWidth="md"
204
+ fullWidth
205
+ >
206
+ <DialogTitle>
207
+ <Typography variant="h6">Annotate Your Correction</Typography>
208
+ <Typography variant="caption" color="text.secondary">
209
+ Help improve the AI by explaining your correction
210
+ </Typography>
211
+ </DialogTitle>
212
+
213
+ <DialogContent dividers>
214
+ {/* Show what changed */}
215
+ <Paper elevation={0} sx={{ p: 2, mb: 3, bgcolor: 'grey.50' }}>
216
+ <Typography variant="subtitle2" gutterBottom>
217
+ Your Correction:
218
+ </Typography>
219
+ <Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
220
+ <Box sx={{ flex: 1 }}>
221
+ <Typography variant="caption" color="error">Original:</Typography>
222
+ <Typography
223
+ sx={{
224
+ textDecoration: 'line-through',
225
+ color: 'error.main',
226
+ fontFamily: 'monospace'
227
+ }}
228
+ >
229
+ {originalText || '(empty)'}
230
+ </Typography>
231
+ </Box>
232
+ <Typography variant="h6">→</Typography>
233
+ <Box sx={{ flex: 1 }}>
234
+ <Typography variant="caption" color="success.main">Corrected:</Typography>
235
+ <Typography
236
+ sx={{
237
+ color: 'success.main',
238
+ fontWeight: 'bold',
239
+ fontFamily: 'monospace'
240
+ }}
241
+ >
242
+ {correctedText || '(empty)'}
243
+ </Typography>
244
+ </Box>
245
+ </Box>
246
+ </Paper>
247
+
248
+ {/* Show AI suggestion if available */}
249
+ {agenticProposal && (
250
+ <Alert
251
+ severity={agenticAgreed ? "success" : "info"}
252
+ sx={{ mb: 3 }}
253
+ >
254
+ <Typography variant="subtitle2" gutterBottom>
255
+ AI Suggestion:
256
+ </Typography>
257
+ <Typography variant="body2">
258
+ Category: <strong>{agenticProposal.gap_category}</strong>
259
+ </Typography>
260
+ <Typography variant="body2">
261
+ Action: <strong>{agenticProposal.action}</strong>
262
+ {agenticProposal.replacement_text && ` → "${agenticProposal.replacement_text}"`}
263
+ </Typography>
264
+ <Typography variant="body2">
265
+ Reason: {agenticProposal.reason}
266
+ </Typography>
267
+ <Typography variant="body2" sx={{ mt: 1 }}>
268
+ {agenticAgreed
269
+ ? '✓ Your correction matches the AI suggestion'
270
+ : '✗ Your correction differs from the AI suggestion'}
271
+ </Typography>
272
+ </Alert>
273
+ )}
274
+
275
+ {/* Reference sources */}
276
+ {referenceSources.length > 0 && (
277
+ <Box sx={{ mb: 3 }}>
278
+ <Typography variant="subtitle2" gutterBottom>
279
+ Reference Sources Consulted:
280
+ </Typography>
281
+ <Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
282
+ {referenceSources.map(source => (
283
+ <Chip key={source} label={source} size="small" />
284
+ ))}
285
+ </Box>
286
+ </Box>
287
+ )}
288
+
289
+ {/* Annotation type */}
290
+ <FormControl fullWidth sx={{ mb: 3 }}>
291
+ <InputLabel>Correction Type *</InputLabel>
292
+ <Select
293
+ value={annotationType}
294
+ label="Correction Type *"
295
+ onChange={(e: SelectChangeEvent) => setAnnotationType(e.target.value as CorrectionAnnotationType)}
296
+ >
297
+ {ANNOTATION_TYPES.map(type => (
298
+ <MenuItem key={type.value} value={type.value}>
299
+ <Box>
300
+ <Typography variant="body1">{type.label}</Typography>
301
+ <Typography variant="caption" color="text.secondary">
302
+ {type.description}
303
+ </Typography>
304
+ </Box>
305
+ </MenuItem>
306
+ ))}
307
+ </Select>
308
+ </FormControl>
309
+
310
+ {/* Confidence slider */}
311
+ <Box sx={{ mb: 3 }}>
312
+ <Typography variant="subtitle2" gutterBottom>
313
+ Confidence in Your Correction *
314
+ </Typography>
315
+ <Slider
316
+ value={confidence}
317
+ onChange={(_, newValue) => setConfidence(newValue as number)}
318
+ min={1}
319
+ max={5}
320
+ step={1}
321
+ marks
322
+ valueLabelDisplay="auto"
323
+ valueLabelFormat={(value) => CONFIDENCE_LABELS[value]}
324
+ />
325
+ <Typography variant="caption" color="text.secondary">
326
+ {CONFIDENCE_LABELS[confidence]}
327
+ </Typography>
328
+ </Box>
329
+
330
+ {/* Reasoning text area */}
331
+ <TextField
332
+ fullWidth
333
+ multiline
334
+ rows={4}
335
+ label="Reasoning *"
336
+ placeholder="Explain why this correction is needed (minimum 10 characters)..."
337
+ value={reasoning}
338
+ onChange={(e) => {
339
+ setReasoning(e.target.value)
340
+ setError('')
341
+ }}
342
+ error={!!error}
343
+ helperText={error || `${reasoning.length}/10 minimum characters`}
344
+ required
345
+ />
346
+ </DialogContent>
347
+
348
+ <DialogActions>
349
+ <Button onClick={handleSkipAndClose} color="inherit">
350
+ Skip
351
+ </Button>
352
+ <Button onClick={handleSave} variant="contained" color="primary">
353
+ Save & Continue
354
+ </Button>
355
+ </DialogActions>
356
+ </Dialog>
357
+ )
358
+ }
359
+