karaoke-gen 0.75.54__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (287) hide show
  1. karaoke_gen/__init__.py +38 -0
  2. karaoke_gen/audio_fetcher.py +1614 -0
  3. karaoke_gen/audio_processor.py +790 -0
  4. karaoke_gen/config.py +83 -0
  5. karaoke_gen/file_handler.py +387 -0
  6. karaoke_gen/instrumental_review/__init__.py +45 -0
  7. karaoke_gen/instrumental_review/analyzer.py +408 -0
  8. karaoke_gen/instrumental_review/editor.py +322 -0
  9. karaoke_gen/instrumental_review/models.py +171 -0
  10. karaoke_gen/instrumental_review/server.py +475 -0
  11. karaoke_gen/instrumental_review/static/index.html +1529 -0
  12. karaoke_gen/instrumental_review/waveform.py +409 -0
  13. karaoke_gen/karaoke_finalise/__init__.py +1 -0
  14. karaoke_gen/karaoke_finalise/karaoke_finalise.py +1833 -0
  15. karaoke_gen/karaoke_gen.py +1026 -0
  16. karaoke_gen/lyrics_processor.py +474 -0
  17. karaoke_gen/metadata.py +160 -0
  18. karaoke_gen/pipeline/__init__.py +87 -0
  19. karaoke_gen/pipeline/base.py +215 -0
  20. karaoke_gen/pipeline/context.py +230 -0
  21. karaoke_gen/pipeline/executors/__init__.py +21 -0
  22. karaoke_gen/pipeline/executors/local.py +159 -0
  23. karaoke_gen/pipeline/executors/remote.py +257 -0
  24. karaoke_gen/pipeline/stages/__init__.py +27 -0
  25. karaoke_gen/pipeline/stages/finalize.py +202 -0
  26. karaoke_gen/pipeline/stages/render.py +165 -0
  27. karaoke_gen/pipeline/stages/screens.py +139 -0
  28. karaoke_gen/pipeline/stages/separation.py +191 -0
  29. karaoke_gen/pipeline/stages/transcription.py +191 -0
  30. karaoke_gen/resources/AvenirNext-Bold.ttf +0 -0
  31. karaoke_gen/resources/Montserrat-Bold.ttf +0 -0
  32. karaoke_gen/resources/Oswald-Bold.ttf +0 -0
  33. karaoke_gen/resources/Oswald-SemiBold.ttf +0 -0
  34. karaoke_gen/resources/Zurich_Cn_BT_Bold.ttf +0 -0
  35. karaoke_gen/style_loader.py +531 -0
  36. karaoke_gen/utils/__init__.py +18 -0
  37. karaoke_gen/utils/bulk_cli.py +492 -0
  38. karaoke_gen/utils/cli_args.py +432 -0
  39. karaoke_gen/utils/gen_cli.py +978 -0
  40. karaoke_gen/utils/remote_cli.py +3268 -0
  41. karaoke_gen/video_background_processor.py +351 -0
  42. karaoke_gen/video_generator.py +424 -0
  43. karaoke_gen-0.75.54.dist-info/METADATA +718 -0
  44. karaoke_gen-0.75.54.dist-info/RECORD +287 -0
  45. karaoke_gen-0.75.54.dist-info/WHEEL +4 -0
  46. karaoke_gen-0.75.54.dist-info/entry_points.txt +5 -0
  47. karaoke_gen-0.75.54.dist-info/licenses/LICENSE +21 -0
  48. lyrics_transcriber/__init__.py +10 -0
  49. lyrics_transcriber/cli/__init__.py +0 -0
  50. lyrics_transcriber/cli/cli_main.py +285 -0
  51. lyrics_transcriber/core/__init__.py +0 -0
  52. lyrics_transcriber/core/config.py +50 -0
  53. lyrics_transcriber/core/controller.py +594 -0
  54. lyrics_transcriber/correction/__init__.py +0 -0
  55. lyrics_transcriber/correction/agentic/__init__.py +9 -0
  56. lyrics_transcriber/correction/agentic/adapter.py +71 -0
  57. lyrics_transcriber/correction/agentic/agent.py +313 -0
  58. lyrics_transcriber/correction/agentic/feedback/aggregator.py +12 -0
  59. lyrics_transcriber/correction/agentic/feedback/collector.py +17 -0
  60. lyrics_transcriber/correction/agentic/feedback/retention.py +24 -0
  61. lyrics_transcriber/correction/agentic/feedback/store.py +76 -0
  62. lyrics_transcriber/correction/agentic/handlers/__init__.py +24 -0
  63. lyrics_transcriber/correction/agentic/handlers/ambiguous.py +44 -0
  64. lyrics_transcriber/correction/agentic/handlers/background_vocals.py +68 -0
  65. lyrics_transcriber/correction/agentic/handlers/base.py +51 -0
  66. lyrics_transcriber/correction/agentic/handlers/complex_multi_error.py +46 -0
  67. lyrics_transcriber/correction/agentic/handlers/extra_words.py +74 -0
  68. lyrics_transcriber/correction/agentic/handlers/no_error.py +42 -0
  69. lyrics_transcriber/correction/agentic/handlers/punctuation.py +44 -0
  70. lyrics_transcriber/correction/agentic/handlers/registry.py +60 -0
  71. lyrics_transcriber/correction/agentic/handlers/repeated_section.py +44 -0
  72. lyrics_transcriber/correction/agentic/handlers/sound_alike.py +126 -0
  73. lyrics_transcriber/correction/agentic/models/__init__.py +5 -0
  74. lyrics_transcriber/correction/agentic/models/ai_correction.py +31 -0
  75. lyrics_transcriber/correction/agentic/models/correction_session.py +30 -0
  76. lyrics_transcriber/correction/agentic/models/enums.py +38 -0
  77. lyrics_transcriber/correction/agentic/models/human_feedback.py +30 -0
  78. lyrics_transcriber/correction/agentic/models/learning_data.py +26 -0
  79. lyrics_transcriber/correction/agentic/models/observability_metrics.py +28 -0
  80. lyrics_transcriber/correction/agentic/models/schemas.py +46 -0
  81. lyrics_transcriber/correction/agentic/models/utils.py +19 -0
  82. lyrics_transcriber/correction/agentic/observability/__init__.py +5 -0
  83. lyrics_transcriber/correction/agentic/observability/langfuse_integration.py +35 -0
  84. lyrics_transcriber/correction/agentic/observability/metrics.py +46 -0
  85. lyrics_transcriber/correction/agentic/observability/performance.py +19 -0
  86. lyrics_transcriber/correction/agentic/prompts/__init__.py +2 -0
  87. lyrics_transcriber/correction/agentic/prompts/classifier.py +227 -0
  88. lyrics_transcriber/correction/agentic/providers/__init__.py +6 -0
  89. lyrics_transcriber/correction/agentic/providers/base.py +36 -0
  90. lyrics_transcriber/correction/agentic/providers/circuit_breaker.py +145 -0
  91. lyrics_transcriber/correction/agentic/providers/config.py +73 -0
  92. lyrics_transcriber/correction/agentic/providers/constants.py +24 -0
  93. lyrics_transcriber/correction/agentic/providers/health.py +28 -0
  94. lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +212 -0
  95. lyrics_transcriber/correction/agentic/providers/model_factory.py +209 -0
  96. lyrics_transcriber/correction/agentic/providers/response_cache.py +218 -0
  97. lyrics_transcriber/correction/agentic/providers/response_parser.py +111 -0
  98. lyrics_transcriber/correction/agentic/providers/retry_executor.py +127 -0
  99. lyrics_transcriber/correction/agentic/router.py +35 -0
  100. lyrics_transcriber/correction/agentic/workflows/__init__.py +5 -0
  101. lyrics_transcriber/correction/agentic/workflows/consensus_workflow.py +24 -0
  102. lyrics_transcriber/correction/agentic/workflows/correction_graph.py +59 -0
  103. lyrics_transcriber/correction/agentic/workflows/feedback_workflow.py +24 -0
  104. lyrics_transcriber/correction/anchor_sequence.py +919 -0
  105. lyrics_transcriber/correction/corrector.py +760 -0
  106. lyrics_transcriber/correction/feedback/__init__.py +2 -0
  107. lyrics_transcriber/correction/feedback/schemas.py +107 -0
  108. lyrics_transcriber/correction/feedback/store.py +236 -0
  109. lyrics_transcriber/correction/handlers/__init__.py +0 -0
  110. lyrics_transcriber/correction/handlers/base.py +52 -0
  111. lyrics_transcriber/correction/handlers/extend_anchor.py +149 -0
  112. lyrics_transcriber/correction/handlers/levenshtein.py +189 -0
  113. lyrics_transcriber/correction/handlers/llm.py +293 -0
  114. lyrics_transcriber/correction/handlers/llm_providers.py +60 -0
  115. lyrics_transcriber/correction/handlers/no_space_punct_match.py +154 -0
  116. lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +85 -0
  117. lyrics_transcriber/correction/handlers/repeat.py +88 -0
  118. lyrics_transcriber/correction/handlers/sound_alike.py +259 -0
  119. lyrics_transcriber/correction/handlers/syllables_match.py +252 -0
  120. lyrics_transcriber/correction/handlers/word_count_match.py +80 -0
  121. lyrics_transcriber/correction/handlers/word_operations.py +187 -0
  122. lyrics_transcriber/correction/operations.py +352 -0
  123. lyrics_transcriber/correction/phrase_analyzer.py +435 -0
  124. lyrics_transcriber/correction/text_utils.py +30 -0
  125. lyrics_transcriber/frontend/.gitignore +23 -0
  126. lyrics_transcriber/frontend/.yarn/releases/yarn-4.7.0.cjs +935 -0
  127. lyrics_transcriber/frontend/.yarnrc.yml +3 -0
  128. lyrics_transcriber/frontend/README.md +50 -0
  129. lyrics_transcriber/frontend/REPLACE_ALL_FUNCTIONALITY.md +210 -0
  130. lyrics_transcriber/frontend/__init__.py +25 -0
  131. lyrics_transcriber/frontend/eslint.config.js +28 -0
  132. lyrics_transcriber/frontend/index.html +18 -0
  133. lyrics_transcriber/frontend/package.json +42 -0
  134. lyrics_transcriber/frontend/public/android-chrome-192x192.png +0 -0
  135. lyrics_transcriber/frontend/public/android-chrome-512x512.png +0 -0
  136. lyrics_transcriber/frontend/public/apple-touch-icon.png +0 -0
  137. lyrics_transcriber/frontend/public/favicon-16x16.png +0 -0
  138. lyrics_transcriber/frontend/public/favicon-32x32.png +0 -0
  139. lyrics_transcriber/frontend/public/favicon.ico +0 -0
  140. lyrics_transcriber/frontend/public/nomad-karaoke-logo.png +0 -0
  141. lyrics_transcriber/frontend/src/App.tsx +214 -0
  142. lyrics_transcriber/frontend/src/api.ts +254 -0
  143. lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +77 -0
  144. lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx +114 -0
  145. lyrics_transcriber/frontend/src/components/AgenticCorrectionMetrics.tsx +204 -0
  146. lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +180 -0
  147. lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +167 -0
  148. lyrics_transcriber/frontend/src/components/CorrectionAnnotationModal.tsx +359 -0
  149. lyrics_transcriber/frontend/src/components/CorrectionDetailCard.tsx +281 -0
  150. lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +162 -0
  151. lyrics_transcriber/frontend/src/components/DurationTimelineView.tsx +257 -0
  152. lyrics_transcriber/frontend/src/components/EditActionBar.tsx +68 -0
  153. lyrics_transcriber/frontend/src/components/EditModal.tsx +702 -0
  154. lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +496 -0
  155. lyrics_transcriber/frontend/src/components/EditWordList.tsx +379 -0
  156. lyrics_transcriber/frontend/src/components/FileUpload.tsx +77 -0
  157. lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +467 -0
  158. lyrics_transcriber/frontend/src/components/Header.tsx +413 -0
  159. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +1387 -0
  160. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +185 -0
  161. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +704 -0
  162. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/UpcomingWordsBar.tsx +80 -0
  163. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +905 -0
  164. lyrics_transcriber/frontend/src/components/MetricsDashboard.tsx +51 -0
  165. lyrics_transcriber/frontend/src/components/ModeSelectionModal.tsx +127 -0
  166. lyrics_transcriber/frontend/src/components/ModeSelector.tsx +67 -0
  167. lyrics_transcriber/frontend/src/components/ModelSelector.tsx +23 -0
  168. lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +144 -0
  169. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +268 -0
  170. lyrics_transcriber/frontend/src/components/ReplaceAllLyricsModal.tsx +336 -0
  171. lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +354 -0
  172. lyrics_transcriber/frontend/src/components/SegmentDetailsModal.tsx +64 -0
  173. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +376 -0
  174. lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +131 -0
  175. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +256 -0
  176. lyrics_transcriber/frontend/src/components/WordDivider.tsx +187 -0
  177. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +379 -0
  178. lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +56 -0
  179. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +87 -0
  180. lyrics_transcriber/frontend/src/components/shared/constants.ts +20 -0
  181. lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +180 -0
  182. lyrics_transcriber/frontend/src/components/shared/styles.ts +13 -0
  183. lyrics_transcriber/frontend/src/components/shared/types.js +2 -0
  184. lyrics_transcriber/frontend/src/components/shared/types.ts +129 -0
  185. lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +177 -0
  186. lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +78 -0
  187. lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +75 -0
  188. lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +360 -0
  189. lyrics_transcriber/frontend/src/components/shared/utils/timingUtils.ts +110 -0
  190. lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +22 -0
  191. lyrics_transcriber/frontend/src/hooks/useManualSync.ts +435 -0
  192. lyrics_transcriber/frontend/src/main.tsx +17 -0
  193. lyrics_transcriber/frontend/src/theme.ts +177 -0
  194. lyrics_transcriber/frontend/src/types/global.d.ts +9 -0
  195. lyrics_transcriber/frontend/src/types.js +2 -0
  196. lyrics_transcriber/frontend/src/types.ts +199 -0
  197. lyrics_transcriber/frontend/src/validation.ts +132 -0
  198. lyrics_transcriber/frontend/src/vite-env.d.ts +1 -0
  199. lyrics_transcriber/frontend/tsconfig.app.json +26 -0
  200. lyrics_transcriber/frontend/tsconfig.json +25 -0
  201. lyrics_transcriber/frontend/tsconfig.node.json +23 -0
  202. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -0
  203. lyrics_transcriber/frontend/update_version.js +11 -0
  204. lyrics_transcriber/frontend/vite.config.d.ts +2 -0
  205. lyrics_transcriber/frontend/vite.config.js +10 -0
  206. lyrics_transcriber/frontend/vite.config.ts +11 -0
  207. lyrics_transcriber/frontend/web_assets/android-chrome-192x192.png +0 -0
  208. lyrics_transcriber/frontend/web_assets/android-chrome-512x512.png +0 -0
  209. lyrics_transcriber/frontend/web_assets/apple-touch-icon.png +0 -0
  210. lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js +43288 -0
  211. lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js.map +1 -0
  212. lyrics_transcriber/frontend/web_assets/favicon-16x16.png +0 -0
  213. lyrics_transcriber/frontend/web_assets/favicon-32x32.png +0 -0
  214. lyrics_transcriber/frontend/web_assets/favicon.ico +0 -0
  215. lyrics_transcriber/frontend/web_assets/index.html +18 -0
  216. lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.png +0 -0
  217. lyrics_transcriber/frontend/yarn.lock +3752 -0
  218. lyrics_transcriber/lyrics/__init__.py +0 -0
  219. lyrics_transcriber/lyrics/base_lyrics_provider.py +211 -0
  220. lyrics_transcriber/lyrics/file_provider.py +95 -0
  221. lyrics_transcriber/lyrics/genius.py +384 -0
  222. lyrics_transcriber/lyrics/lrclib.py +231 -0
  223. lyrics_transcriber/lyrics/musixmatch.py +156 -0
  224. lyrics_transcriber/lyrics/spotify.py +290 -0
  225. lyrics_transcriber/lyrics/user_input_provider.py +44 -0
  226. lyrics_transcriber/output/__init__.py +0 -0
  227. lyrics_transcriber/output/ass/__init__.py +21 -0
  228. lyrics_transcriber/output/ass/ass.py +2088 -0
  229. lyrics_transcriber/output/ass/ass_specs.txt +732 -0
  230. lyrics_transcriber/output/ass/config.py +180 -0
  231. lyrics_transcriber/output/ass/constants.py +23 -0
  232. lyrics_transcriber/output/ass/event.py +94 -0
  233. lyrics_transcriber/output/ass/formatters.py +132 -0
  234. lyrics_transcriber/output/ass/lyrics_line.py +265 -0
  235. lyrics_transcriber/output/ass/lyrics_screen.py +252 -0
  236. lyrics_transcriber/output/ass/section_detector.py +89 -0
  237. lyrics_transcriber/output/ass/section_screen.py +106 -0
  238. lyrics_transcriber/output/ass/style.py +187 -0
  239. lyrics_transcriber/output/cdg.py +619 -0
  240. lyrics_transcriber/output/cdgmaker/__init__.py +0 -0
  241. lyrics_transcriber/output/cdgmaker/cdg.py +262 -0
  242. lyrics_transcriber/output/cdgmaker/composer.py +2260 -0
  243. lyrics_transcriber/output/cdgmaker/config.py +151 -0
  244. lyrics_transcriber/output/cdgmaker/images/instrumental.png +0 -0
  245. lyrics_transcriber/output/cdgmaker/images/intro.png +0 -0
  246. lyrics_transcriber/output/cdgmaker/pack.py +507 -0
  247. lyrics_transcriber/output/cdgmaker/render.py +346 -0
  248. lyrics_transcriber/output/cdgmaker/transitions/centertexttoplogobottomtext.png +0 -0
  249. lyrics_transcriber/output/cdgmaker/transitions/circlein.png +0 -0
  250. lyrics_transcriber/output/cdgmaker/transitions/circleout.png +0 -0
  251. lyrics_transcriber/output/cdgmaker/transitions/fizzle.png +0 -0
  252. lyrics_transcriber/output/cdgmaker/transitions/largecentertexttoplogo.png +0 -0
  253. lyrics_transcriber/output/cdgmaker/transitions/rectangle.png +0 -0
  254. lyrics_transcriber/output/cdgmaker/transitions/spiral.png +0 -0
  255. lyrics_transcriber/output/cdgmaker/transitions/topleftmusicalnotes.png +0 -0
  256. lyrics_transcriber/output/cdgmaker/transitions/wipein.png +0 -0
  257. lyrics_transcriber/output/cdgmaker/transitions/wipeleft.png +0 -0
  258. lyrics_transcriber/output/cdgmaker/transitions/wipeout.png +0 -0
  259. lyrics_transcriber/output/cdgmaker/transitions/wiperight.png +0 -0
  260. lyrics_transcriber/output/cdgmaker/utils.py +132 -0
  261. lyrics_transcriber/output/countdown_processor.py +306 -0
  262. lyrics_transcriber/output/fonts/AvenirNext-Bold.ttf +0 -0
  263. lyrics_transcriber/output/fonts/DMSans-VariableFont_opsz,wght.ttf +0 -0
  264. lyrics_transcriber/output/fonts/DMSerifDisplay-Regular.ttf +0 -0
  265. lyrics_transcriber/output/fonts/Oswald-SemiBold.ttf +0 -0
  266. lyrics_transcriber/output/fonts/Zurich_Cn_BT_Bold.ttf +0 -0
  267. lyrics_transcriber/output/fonts/arial.ttf +0 -0
  268. lyrics_transcriber/output/fonts/georgia.ttf +0 -0
  269. lyrics_transcriber/output/fonts/verdana.ttf +0 -0
  270. lyrics_transcriber/output/generator.py +257 -0
  271. lyrics_transcriber/output/lrc_to_cdg.py +61 -0
  272. lyrics_transcriber/output/lyrics_file.py +102 -0
  273. lyrics_transcriber/output/plain_text.py +96 -0
  274. lyrics_transcriber/output/segment_resizer.py +431 -0
  275. lyrics_transcriber/output/subtitles.py +397 -0
  276. lyrics_transcriber/output/video.py +544 -0
  277. lyrics_transcriber/review/__init__.py +0 -0
  278. lyrics_transcriber/review/server.py +676 -0
  279. lyrics_transcriber/storage/__init__.py +0 -0
  280. lyrics_transcriber/storage/dropbox.py +225 -0
  281. lyrics_transcriber/transcribers/__init__.py +0 -0
  282. lyrics_transcriber/transcribers/audioshake.py +379 -0
  283. lyrics_transcriber/transcribers/base_transcriber.py +157 -0
  284. lyrics_transcriber/transcribers/whisper.py +330 -0
  285. lyrics_transcriber/types.py +650 -0
  286. lyrics_transcriber/utils/__init__.py +0 -0
  287. lyrics_transcriber/utils/word_utils.py +27 -0
@@ -0,0 +1,336 @@
1
+ import {
2
+ Dialog,
3
+ DialogTitle,
4
+ DialogContent,
5
+ DialogActions,
6
+ IconButton,
7
+ Box,
8
+ Button,
9
+ Typography,
10
+ TextField
11
+ } from '@mui/material'
12
+ import CloseIcon from '@mui/icons-material/Close'
13
+ import ContentPasteIcon from '@mui/icons-material/ContentPaste'
14
+ import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh'
15
+ import ArrowBackIcon from '@mui/icons-material/ArrowBack'
16
+ import { LyricsSegment, Word } from '../types'
17
+ import { useState, useEffect, useCallback, useMemo } from 'react'
18
+ import { nanoid } from 'nanoid'
19
+ import ModeSelectionModal from './ModeSelectionModal'
20
+ import LyricsSynchronizer from './LyricsSynchronizer'
21
+
22
+ // Augment window type for audio functions
23
+ declare global {
24
+ interface Window {
25
+ getAudioDuration?: () => number;
26
+ toggleAudioPlayback?: () => void;
27
+ isAudioPlaying?: boolean;
28
+ }
29
+ }
30
+
31
+ type ModalMode = 'selection' | 'replace' | 'resync'
32
+
33
+ interface ReplaceAllLyricsModalProps {
34
+ open: boolean
35
+ onClose: () => void
36
+ onSave: (newSegments: LyricsSegment[]) => void
37
+ onPlaySegment?: (startTime: number) => void
38
+ currentTime?: number
39
+ setModalSpacebarHandler: (handler: (() => (e: KeyboardEvent) => void) | undefined) => void
40
+ existingSegments?: LyricsSegment[] // Current segments for re-sync mode
41
+ }
42
+
43
+ export default function ReplaceAllLyricsModal({
44
+ open,
45
+ onClose,
46
+ onSave,
47
+ onPlaySegment,
48
+ currentTime = 0,
49
+ setModalSpacebarHandler,
50
+ existingSegments = []
51
+ }: ReplaceAllLyricsModalProps) {
52
+ const [mode, setMode] = useState<ModalMode>('selection')
53
+ const [inputText, setInputText] = useState('')
54
+ const [newSegments, setNewSegments] = useState<LyricsSegment[]>([])
55
+
56
+ // Reset state when modal opens
57
+ useEffect(() => {
58
+ if (open) {
59
+ setMode('selection')
60
+ setInputText('')
61
+ setNewSegments([])
62
+ }
63
+ }, [open])
64
+
65
+ // Parse the input text to get line and word counts
66
+ const parseInfo = useMemo(() => {
67
+ if (!inputText.trim()) return { lines: 0, words: 0 }
68
+
69
+ const lines = inputText.trim().split('\n').filter(line => line.trim().length > 0)
70
+ const totalWords = lines.reduce((count, line) => {
71
+ return count + line.trim().split(/\s+/).length
72
+ }, 0)
73
+
74
+ return { lines: lines.length, words: totalWords }
75
+ }, [inputText])
76
+
77
+ // Process the input text into segments and words
78
+ const processLyrics = useCallback(() => {
79
+ if (!inputText.trim()) return
80
+
81
+ const lines = inputText.trim().split('\n').filter(line => line.trim().length > 0)
82
+ const segments: LyricsSegment[] = []
83
+
84
+ lines.forEach((line) => {
85
+ const words = line.trim().split(/\s+/).filter(word => word.length > 0)
86
+ const segmentWords: Word[] = words.map((wordText) => ({
87
+ id: nanoid(),
88
+ text: wordText,
89
+ start_time: null,
90
+ end_time: null,
91
+ confidence: 1.0,
92
+ created_during_correction: true
93
+ }))
94
+
95
+ segments.push({
96
+ id: nanoid(),
97
+ text: line.trim(),
98
+ words: segmentWords,
99
+ start_time: null,
100
+ end_time: null
101
+ })
102
+ })
103
+
104
+ setNewSegments(segments)
105
+ setMode('resync') // Go to synchronizer with the new segments
106
+ }, [inputText])
107
+
108
+ // Handle paste from clipboard
109
+ const handlePasteFromClipboard = useCallback(async () => {
110
+ try {
111
+ const text = await navigator.clipboard.readText()
112
+ setInputText(text)
113
+ } catch (error) {
114
+ console.error('Failed to read from clipboard:', error)
115
+ alert('Failed to read from clipboard. Please paste manually.')
116
+ }
117
+ }, [])
118
+
119
+ // Handle modal close
120
+ const handleClose = useCallback(() => {
121
+ setMode('selection')
122
+ setInputText('')
123
+ setNewSegments([])
124
+ onClose()
125
+ }, [onClose])
126
+
127
+ // Handle save from synchronizer
128
+ const handleSave = useCallback((segments: LyricsSegment[]) => {
129
+ onSave(segments)
130
+ handleClose()
131
+ }, [onSave, handleClose])
132
+
133
+ // Handle mode selection
134
+ const handleSelectReplace = useCallback(() => {
135
+ setMode('replace')
136
+ }, [])
137
+
138
+ const handleSelectResync = useCallback(() => {
139
+ setMode('resync')
140
+ }, [])
141
+
142
+ // Handle back to selection
143
+ const handleBackToSelection = useCallback(() => {
144
+ setMode('selection')
145
+ setInputText('')
146
+ setNewSegments([])
147
+ }, [])
148
+
149
+ // Determine which segments to use for synchronizer
150
+ const segmentsForSync = mode === 'resync' && newSegments.length > 0
151
+ ? newSegments
152
+ : existingSegments
153
+
154
+ // Check if we have existing lyrics
155
+ const hasExistingLyrics = existingSegments.length > 0 &&
156
+ existingSegments.some(s => s.words.length > 0)
157
+
158
+ return (
159
+ <>
160
+ {/* Mode Selection Modal */}
161
+ <ModeSelectionModal
162
+ open={open && mode === 'selection'}
163
+ onClose={handleClose}
164
+ onSelectReplace={handleSelectReplace}
165
+ onSelectResync={handleSelectResync}
166
+ hasExistingLyrics={hasExistingLyrics}
167
+ />
168
+
169
+ {/* Replace All Lyrics Modal (Paste Phase) */}
170
+ <Dialog
171
+ open={open && mode === 'replace'}
172
+ onClose={handleClose}
173
+ maxWidth="md"
174
+ fullWidth
175
+ PaperProps={{
176
+ sx: {
177
+ height: '80vh',
178
+ maxHeight: '80vh'
179
+ }
180
+ }}
181
+ >
182
+ <DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
183
+ <IconButton onClick={handleBackToSelection} size="small">
184
+ <ArrowBackIcon />
185
+ </IconButton>
186
+ <Box sx={{ flex: 1 }}>
187
+ Replace All Lyrics
188
+ </Box>
189
+ <IconButton onClick={handleClose} sx={{ ml: 'auto' }}>
190
+ <CloseIcon />
191
+ </IconButton>
192
+ </DialogTitle>
193
+
194
+ <DialogContent
195
+ dividers
196
+ sx={{
197
+ display: 'flex',
198
+ flexDirection: 'column',
199
+ flexGrow: 1,
200
+ overflow: 'hidden'
201
+ }}
202
+ >
203
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, height: '100%' }}>
204
+ <Typography variant="h6" gutterBottom>
205
+ Paste your new lyrics below:
206
+ </Typography>
207
+
208
+ <Typography variant="body2" color="text.secondary" gutterBottom>
209
+ Each line will become a separate segment. Words will be separated by spaces.
210
+ </Typography>
211
+
212
+ <Box sx={{ display: 'flex', gap: 2, mb: 2 }}>
213
+ <Button
214
+ variant="outlined"
215
+ onClick={handlePasteFromClipboard}
216
+ startIcon={<ContentPasteIcon />}
217
+ size="small"
218
+ >
219
+ Paste from Clipboard
220
+ </Button>
221
+ <Typography variant="body2" sx={{
222
+ alignSelf: 'center',
223
+ color: 'text.secondary',
224
+ fontWeight: 'medium'
225
+ }}>
226
+ {parseInfo.lines} lines, {parseInfo.words} words
227
+ </Typography>
228
+ </Box>
229
+
230
+ <TextField
231
+ multiline
232
+ rows={15}
233
+ value={inputText}
234
+ onChange={(e) => setInputText(e.target.value)}
235
+ placeholder="Paste your lyrics here...&#10;Each line will become a segment&#10;Words will be separated by spaces"
236
+ sx={{
237
+ flexGrow: 1,
238
+ '& .MuiInputBase-root': {
239
+ height: '100%',
240
+ alignItems: 'flex-start'
241
+ }
242
+ }}
243
+ />
244
+ </Box>
245
+ </DialogContent>
246
+
247
+ <DialogActions>
248
+ <Button onClick={handleClose} color="inherit">
249
+ Cancel
250
+ </Button>
251
+ <Button
252
+ variant="contained"
253
+ onClick={processLyrics}
254
+ disabled={!inputText.trim()}
255
+ startIcon={<AutoFixHighIcon />}
256
+ >
257
+ Continue to Sync
258
+ </Button>
259
+ </DialogActions>
260
+ </Dialog>
261
+
262
+ {/* Synchronizer Modal */}
263
+ <Dialog
264
+ open={open && mode === 'resync'}
265
+ onClose={handleClose}
266
+ maxWidth={false}
267
+ fullWidth
268
+ PaperProps={{
269
+ sx: {
270
+ height: '90vh',
271
+ margin: '5vh 2vw',
272
+ maxWidth: 'calc(100vw - 4vw)',
273
+ width: 'calc(100vw - 4vw)'
274
+ }
275
+ }}
276
+ >
277
+ <DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
278
+ <IconButton onClick={handleBackToSelection} size="small">
279
+ <ArrowBackIcon />
280
+ </IconButton>
281
+ <Box sx={{ flex: 1 }}>
282
+ {newSegments.length > 0 ? 'Sync New Lyrics' : 'Re-sync Existing Lyrics'}
283
+ </Box>
284
+ <IconButton onClick={handleClose} sx={{ ml: 'auto' }}>
285
+ <CloseIcon />
286
+ </IconButton>
287
+ </DialogTitle>
288
+
289
+ <DialogContent
290
+ dividers
291
+ sx={{
292
+ display: 'flex',
293
+ flexDirection: 'column',
294
+ flexGrow: 1,
295
+ overflow: 'hidden',
296
+ p: 2
297
+ }}
298
+ >
299
+ {segmentsForSync.length > 0 ? (
300
+ <LyricsSynchronizer
301
+ segments={segmentsForSync}
302
+ currentTime={currentTime}
303
+ onPlaySegment={onPlaySegment}
304
+ onSave={handleSave}
305
+ onCancel={handleClose}
306
+ setModalSpacebarHandler={setModalSpacebarHandler}
307
+ />
308
+ ) : (
309
+ <Box sx={{
310
+ display: 'flex',
311
+ flexDirection: 'column',
312
+ alignItems: 'center',
313
+ justifyContent: 'center',
314
+ height: '100%',
315
+ gap: 2
316
+ }}>
317
+ <Typography variant="h6" color="text.secondary">
318
+ No lyrics to sync
319
+ </Typography>
320
+ <Typography variant="body2" color="text.secondary">
321
+ Go back and paste new lyrics, or close this modal.
322
+ </Typography>
323
+ <Button
324
+ variant="outlined"
325
+ onClick={handleBackToSelection}
326
+ startIcon={<ArrowBackIcon />}
327
+ >
328
+ Back to Selection
329
+ </Button>
330
+ </Box>
331
+ )}
332
+ </DialogContent>
333
+ </Dialog>
334
+ </>
335
+ )
336
+ }
@@ -0,0 +1,354 @@
1
+ import {
2
+ Dialog,
3
+ DialogTitle,
4
+ DialogContent,
5
+ DialogActions,
6
+ Button,
7
+ Box,
8
+ Typography,
9
+ Paper
10
+ } from '@mui/material'
11
+ import { CorrectionData } from '../types'
12
+ import { useMemo, useRef, useEffect } from 'react'
13
+ import { ApiClient } from '../api'
14
+ import PreviewVideoSection from './PreviewVideoSection'
15
+ import { CloudUpload, ArrowBack } from '@mui/icons-material'
16
+
17
+ interface ReviewChangesModalProps {
18
+ open: boolean
19
+ onClose: () => void
20
+ originalData: CorrectionData
21
+ updatedData: CorrectionData
22
+ onSubmit: () => void
23
+ apiClient: ApiClient | null
24
+ setModalSpacebarHandler: (handler: (() => (e: KeyboardEvent) => void) | undefined) => void
25
+ timingOffsetMs?: number
26
+ }
27
+
28
+ interface DiffResult {
29
+ type: 'added' | 'removed' | 'modified'
30
+ path: string
31
+ segmentIndex?: number
32
+ oldValue?: string
33
+ newValue?: string
34
+ wordChanges?: DiffResult[]
35
+ }
36
+
37
+ // Add interfaces for the word and segment structures
38
+ interface Word {
39
+ text: string
40
+ start_time: number | null
41
+ end_time: number | null
42
+ id?: string
43
+ }
44
+
45
+ interface Segment {
46
+ text: string
47
+ start_time: number | null
48
+ end_time: number | null
49
+ words: Word[]
50
+ id?: string
51
+ }
52
+
53
+ const normalizeWordForComparison = (word: Word): Omit<Word, 'id'> => ({
54
+ text: word.text,
55
+ start_time: word.start_time ?? 0, // Default to 0 for comparison
56
+ end_time: word.end_time ?? 0 // Default to 0 for comparison
57
+ })
58
+
59
+ const normalizeSegmentForComparison = (segment: Segment): Omit<Segment, 'id'> => ({
60
+ text: segment.text,
61
+ start_time: segment.start_time ?? 0, // Default to 0 for comparison
62
+ end_time: segment.end_time ?? 0, // Default to 0 for comparison
63
+ words: segment.words.map(normalizeWordForComparison)
64
+ })
65
+
66
+ export default function ReviewChangesModal({
67
+ open,
68
+ onClose,
69
+ originalData,
70
+ updatedData,
71
+ onSubmit,
72
+ apiClient,
73
+ setModalSpacebarHandler,
74
+ timingOffsetMs = 0
75
+ }: ReviewChangesModalProps) {
76
+ // Add ref to video element
77
+ const videoRef = useRef<HTMLVideoElement>(null)
78
+
79
+ // Stop audio playback when modal opens
80
+ useEffect(() => {
81
+ if (open && window.isAudioPlaying && window.toggleAudioPlayback) {
82
+ window.toggleAudioPlayback()
83
+ }
84
+ }, [open])
85
+
86
+ // Add effect to handle spacebar
87
+ useEffect(() => {
88
+ if (open) {
89
+ setModalSpacebarHandler(() => (e: KeyboardEvent) => {
90
+ if (e.type === 'keydown') {
91
+ e.preventDefault()
92
+ e.stopPropagation()
93
+
94
+ if (videoRef.current) {
95
+ if (videoRef.current.paused) {
96
+ videoRef.current.play()
97
+ } else {
98
+ videoRef.current.pause()
99
+ }
100
+ }
101
+ }
102
+ })
103
+ } else {
104
+ setModalSpacebarHandler(undefined)
105
+ }
106
+
107
+ return () => {
108
+ setModalSpacebarHandler(undefined)
109
+ }
110
+ }, [open, setModalSpacebarHandler])
111
+
112
+ // Debug logging for timing offset
113
+ useEffect(() => {
114
+ if (open) {
115
+ console.log(`[TIMING] ReviewChangesModal opened - timingOffsetMs: ${timingOffsetMs}ms`);
116
+ }
117
+ }, [open, timingOffsetMs]);
118
+
119
+ const differences = useMemo(() => {
120
+ const diffs: DiffResult[] = []
121
+
122
+ originalData.corrected_segments.forEach((originalSegment, index) => {
123
+ const updatedSegment = updatedData.corrected_segments[index]
124
+ if (!updatedSegment) {
125
+ diffs.push({
126
+ type: 'removed',
127
+ path: `Segment ${index}`,
128
+ segmentIndex: index,
129
+ oldValue: originalSegment.text
130
+ })
131
+ return
132
+ }
133
+
134
+ const normalizedOriginal = normalizeSegmentForComparison(originalSegment)
135
+ const normalizedUpdated = normalizeSegmentForComparison(updatedSegment)
136
+ const wordChanges: DiffResult[] = []
137
+
138
+ // Compare word-level changes
139
+ normalizedOriginal.words.forEach((word, wordIndex) => {
140
+ const updatedWord = normalizedUpdated.words[wordIndex]
141
+ if (!updatedWord) {
142
+ wordChanges.push({
143
+ type: 'removed',
144
+ path: `Word ${wordIndex}`,
145
+ oldValue: `"${word.text}" (${word.start_time?.toFixed(4) ?? 'N/A'} - ${word.end_time?.toFixed(4) ?? 'N/A'})`
146
+ })
147
+ return
148
+ }
149
+
150
+ if (word.text !== updatedWord.text ||
151
+ Math.abs((word.start_time ?? 0) - (updatedWord.start_time ?? 0)) > 0.0001 ||
152
+ Math.abs((word.end_time ?? 0) - (updatedWord.end_time ?? 0)) > 0.0001) {
153
+ wordChanges.push({
154
+ type: 'modified',
155
+ path: `Word ${wordIndex}`,
156
+ oldValue: `"${word.text}" (${word.start_time?.toFixed(4) ?? 'N/A'} - ${word.end_time?.toFixed(4) ?? 'N/A'})`,
157
+ newValue: `"${updatedWord.text}" (${updatedWord.start_time?.toFixed(4) ?? 'N/A'} - ${updatedWord.end_time?.toFixed(4) ?? 'N/A'})`
158
+ })
159
+ }
160
+ })
161
+
162
+ // Check for added words
163
+ if (normalizedUpdated.words.length > normalizedOriginal.words.length) {
164
+ for (let i = normalizedOriginal.words.length; i < normalizedUpdated.words.length; i++) {
165
+ const word = normalizedUpdated.words[i]
166
+ wordChanges.push({
167
+ type: 'added',
168
+ path: `Word ${i}`,
169
+ newValue: `"${word.text}" (${word.start_time?.toFixed(4) ?? 'N/A'} - ${word.end_time?.toFixed(4) ?? 'N/A'})`
170
+ })
171
+ }
172
+ }
173
+
174
+ // Compare segment-level changes
175
+ if (normalizedOriginal.text !== normalizedUpdated.text ||
176
+ Math.abs((normalizedOriginal.start_time ?? 0) - (normalizedUpdated.start_time ?? 0)) > 0.0001 ||
177
+ Math.abs((normalizedOriginal.end_time ?? 0) - (normalizedUpdated.end_time ?? 0)) > 0.0001 ||
178
+ wordChanges.length > 0) {
179
+ diffs.push({
180
+ type: 'modified',
181
+ path: `Segment ${index}`,
182
+ segmentIndex: index,
183
+ oldValue: `"${normalizedOriginal.text}" (${normalizedOriginal.start_time?.toFixed(4) ?? 'N/A'} - ${normalizedOriginal.end_time?.toFixed(4) ?? 'N/A'})`,
184
+ newValue: `"${normalizedUpdated.text}" (${normalizedUpdated.start_time?.toFixed(4) ?? 'N/A'} - ${normalizedUpdated.end_time?.toFixed(4) ?? 'N/A'})`,
185
+ wordChanges: wordChanges.length > 0 ? wordChanges : undefined
186
+ })
187
+ }
188
+ })
189
+
190
+ // Check for added segments
191
+ if (updatedData.corrected_segments.length > originalData.corrected_segments.length) {
192
+ for (let i = originalData.corrected_segments.length; i < updatedData.corrected_segments.length; i++) {
193
+ const segment = updatedData.corrected_segments[i]
194
+ diffs.push({
195
+ type: 'added',
196
+ path: `Segment ${i}`,
197
+ segmentIndex: i,
198
+ newValue: `"${segment.text}" (${segment.start_time?.toFixed(4) ?? 'N/A'} - ${segment.end_time?.toFixed(4) ?? 'N/A'})`
199
+ })
200
+ }
201
+ }
202
+
203
+ return diffs
204
+ }, [originalData, updatedData])
205
+
206
+ const renderCompactDiff = (diff: DiffResult) => {
207
+ if (diff.type !== 'modified') {
208
+ // For added/removed segments, show them as before but in a single line
209
+ return (
210
+ <Typography
211
+ key={diff.path}
212
+ color={diff.type === 'added' ? 'success.main' : 'error.main'}
213
+ sx={{ mb: 0.5 }}
214
+ >
215
+ {diff.segmentIndex}: {diff.type === 'added' ? '+ ' : '- '}
216
+ {diff.type === 'added' ? diff.newValue : diff.oldValue}
217
+ </Typography>
218
+ )
219
+ }
220
+
221
+ // For modified segments, create a unified inline diff view
222
+ const oldText = diff.oldValue?.split('"')[1] || ''
223
+ const newText = diff.newValue?.split('"')[1] || ''
224
+ const oldWords = oldText.split(' ')
225
+ const newWords = newText.split(' ')
226
+
227
+ // Extract timing info and format with 2 decimal places
228
+ const timingMatch = diff.newValue?.match(/\(([\d.]+) - ([\d.]+)\)/)
229
+ const timing = timingMatch ?
230
+ `(${parseFloat(timingMatch[1]).toFixed(2)} - ${parseFloat(timingMatch[2]).toFixed(2)})` :
231
+ ''
232
+
233
+ // Create unified diff of words
234
+ const unifiedDiff = []
235
+ let i = 0, j = 0
236
+
237
+ while (i < oldWords.length || j < newWords.length) {
238
+ if (i < oldWords.length && j < newWords.length && oldWords[i] === newWords[j]) {
239
+ // Unchanged word
240
+ unifiedDiff.push({ type: 'unchanged', text: oldWords[i] })
241
+ i++
242
+ j++
243
+ } else if (i < oldWords.length && (!newWords[j] || oldWords[i] !== newWords[j])) {
244
+ // Deleted word
245
+ unifiedDiff.push({ type: 'deleted', text: oldWords[i] })
246
+ i++
247
+ } else if (j < newWords.length) {
248
+ // Added word
249
+ unifiedDiff.push({ type: 'added', text: newWords[j] })
250
+ j++
251
+ }
252
+ }
253
+
254
+ return (
255
+ <Box key={diff.path} sx={{ mb: 0.5, display: 'flex', alignItems: 'center' }}>
256
+ <Typography variant="body2" color="text.secondary" sx={{ mr: 1, minWidth: '30px' }}>
257
+ {diff.segmentIndex}:
258
+ </Typography>
259
+ <Box sx={{ display: 'flex', flexWrap: 'wrap', flexGrow: 1, alignItems: 'center' }}>
260
+ {unifiedDiff.map((word, idx) => (
261
+ <Typography
262
+ key={idx}
263
+ component="span"
264
+ color={
265
+ word.type === 'unchanged' ? 'text.primary' :
266
+ word.type === 'deleted' ? 'error.main' : 'success.main'
267
+ }
268
+ sx={{
269
+ textDecoration: word.type === 'deleted' ? 'line-through' : 'none',
270
+ mr: 0.5
271
+ }}
272
+ >
273
+ {word.text}
274
+ </Typography>
275
+ ))}
276
+ <Typography variant="body2" color="text.secondary" sx={{ ml: 1 }}>
277
+ {timing}
278
+ </Typography>
279
+ </Box>
280
+ </Box>
281
+ )
282
+ }
283
+
284
+ return (
285
+ <Dialog
286
+ open={open}
287
+ onClose={onClose}
288
+ maxWidth="md"
289
+ fullWidth
290
+ >
291
+ <DialogTitle>Preview Video (With Vocals)</DialogTitle>
292
+ <DialogContent
293
+ dividers
294
+ sx={{
295
+ p: 0, // Remove default padding
296
+ '&:first-of-type': { pt: 0 } // Remove default top padding
297
+ }}
298
+ >
299
+ <PreviewVideoSection
300
+ apiClient={apiClient}
301
+ isModalOpen={open}
302
+ updatedData={updatedData}
303
+ videoRef={videoRef} // Pass the ref to PreviewVideoSection
304
+ timingOffsetMs={timingOffsetMs}
305
+ />
306
+
307
+ <Box sx={{ p: 2, mt: 0 }}>
308
+ {timingOffsetMs !== 0 && (
309
+ <Typography variant="body2" fontWeight="bold" sx={{ mt: 1 }}>
310
+ Global Timing Offset applied to all words: {timingOffsetMs > 0 ? '+' : ''}{timingOffsetMs}ms
311
+ </Typography>
312
+ )}
313
+ {differences.length === 0 ? (
314
+ <Box>
315
+ <Typography color="text.secondary">
316
+ No manual corrections detected. If everything looks good in the preview, click submit and the server will generate the final karaoke video.
317
+ </Typography>
318
+ <Typography variant="body2" color="text.secondary">
319
+ Total segments: {updatedData.corrected_segments.length}
320
+ </Typography>
321
+ </Box>
322
+ ) : (
323
+ <Box>
324
+ <Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
325
+ {differences.length} segment{differences.length !== 1 ? 's' : ''} modified:
326
+ </Typography>
327
+ <Paper sx={{ p: 2 }}>
328
+ {differences.map(renderCompactDiff)}
329
+ </Paper>
330
+ </Box>
331
+ )}
332
+ </Box>
333
+ </DialogContent>
334
+ <DialogActions>
335
+ <Button
336
+ onClick={onClose}
337
+ color="warning"
338
+ startIcon={<ArrowBack />}
339
+ sx={{ mr: 'auto' }}
340
+ >
341
+ Cancel
342
+ </Button>
343
+ <Button
344
+ onClick={onSubmit}
345
+ variant="contained"
346
+ color="success"
347
+ endIcon={<CloudUpload />}
348
+ >
349
+ Complete Review
350
+ </Button>
351
+ </DialogActions>
352
+ </Dialog>
353
+ )
354
+ }