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,64 @@
1
+ import {
2
+ Dialog,
3
+ DialogTitle,
4
+ DialogContent,
5
+ IconButton, Box
6
+ } from '@mui/material'
7
+ import CloseIcon from '@mui/icons-material/Close'
8
+ import { LyricsSegment } from '../types'
9
+
10
+ interface SegmentDetailsModalProps {
11
+ open: boolean
12
+ onClose: () => void
13
+ segment: LyricsSegment | null
14
+ segmentIndex: number | null
15
+ }
16
+
17
+ export default function SegmentDetailsModal({
18
+ open,
19
+ onClose,
20
+ segment,
21
+ segmentIndex
22
+ }: SegmentDetailsModalProps) {
23
+ if (!segment || segmentIndex === null) return null
24
+
25
+ return (
26
+ <Dialog
27
+ open={open}
28
+ onClose={onClose}
29
+ maxWidth="sm"
30
+ fullWidth
31
+ PaperProps={{
32
+ sx: { position: 'relative' },
33
+ }}
34
+ >
35
+ <IconButton
36
+ onClick={onClose}
37
+ sx={{
38
+ position: 'absolute',
39
+ right: 8,
40
+ top: 8,
41
+ }}
42
+ >
43
+ <CloseIcon />
44
+ </IconButton>
45
+ <DialogTitle>
46
+ Segment {segmentIndex} Details
47
+ </DialogTitle>
48
+ <DialogContent dividers>
49
+ <Box
50
+ component="pre"
51
+ sx={{
52
+ margin: 0,
53
+ fontFamily: 'monospace',
54
+ fontSize: '0.875rem',
55
+ whiteSpace: 'pre-wrap',
56
+ wordBreak: 'break-word'
57
+ }}
58
+ >
59
+ {JSON.stringify(segment, null, 2)}
60
+ </Box>
61
+ </DialogContent>
62
+ </Dialog>
63
+ )
64
+ }
@@ -0,0 +1,376 @@
1
+ import { Box, styled } from '@mui/material'
2
+ import { Word } from '../types'
3
+ import { useRef, useState } from 'react'
4
+
5
+ interface TimelineEditorProps {
6
+ words: Word[]
7
+ startTime: number
8
+ endTime: number
9
+ onWordUpdate: (index: number, updates: Partial<Word>) => void
10
+ onUnsyncWord?: (index: number) => void
11
+ currentTime?: number
12
+ onPlaySegment?: (time: number) => void
13
+ showPlaybackIndicator?: boolean
14
+ }
15
+
16
+ const TimelineContainer = styled(Box)(({ theme }) => ({
17
+ position: 'relative',
18
+ height: '75px',
19
+ backgroundColor: theme.palette.grey[200],
20
+ borderRadius: theme.shape.borderRadius,
21
+ margin: theme.spacing(1, 0),
22
+ padding: theme.spacing(0, 1),
23
+ }))
24
+
25
+ const TimelineRuler = styled(Box)(({ theme }) => ({
26
+ position: 'absolute',
27
+ top: 0,
28
+ left: 0,
29
+ right: 0,
30
+ height: '40px',
31
+ borderBottom: `1px solid ${theme.palette.grey[300]}`,
32
+ cursor: 'pointer',
33
+ }))
34
+
35
+ const TimelineMark = styled(Box)(({ theme }) => ({
36
+ position: 'absolute',
37
+ top: '20px',
38
+ width: '1px',
39
+ height: '18px',
40
+ backgroundColor: theme.palette.grey[700],
41
+ '&.subsecond': {
42
+ top: '25px',
43
+ height: '13px',
44
+ backgroundColor: theme.palette.grey[500],
45
+ }
46
+ }))
47
+
48
+ const TimelineLabel = styled(Box)(({ theme }) => ({
49
+ position: 'absolute',
50
+ top: '5px',
51
+ transform: 'translateX(-50%)',
52
+ fontSize: '0.8rem',
53
+ color: theme.palette.text.primary,
54
+ fontWeight: 700,
55
+ backgroundColor: theme.palette.grey[200],
56
+ }))
57
+
58
+ const TimelineWord = styled(Box)(({ theme }) => ({
59
+ position: 'absolute',
60
+ height: '30px',
61
+ top: '40px',
62
+ backgroundColor: theme.palette.primary.main,
63
+ borderRadius: theme.shape.borderRadius,
64
+ color: theme.palette.primary.contrastText,
65
+ padding: theme.spacing(0.5, 1),
66
+ cursor: 'move',
67
+ userSelect: 'none',
68
+ display: 'flex',
69
+ alignItems: 'center',
70
+ fontSize: '0.875rem',
71
+ fontFamily: 'sans-serif',
72
+ transition: 'background-color 0.1s ease',
73
+ boxSizing: 'border-box',
74
+ '&.highlighted': {
75
+ backgroundColor: theme.palette.secondary.main,
76
+ }
77
+ }))
78
+
79
+ const ResizeHandle = styled(Box)(({ theme }) => ({
80
+ position: 'absolute',
81
+ top: 0,
82
+ width: 10,
83
+ height: '100%',
84
+ cursor: 'col-resize',
85
+ '&:hover': {
86
+ backgroundColor: theme.palette.primary.light,
87
+ opacity: 0.8,
88
+ boxShadow: `0 0 0 1px ${theme.palette.primary.dark}`,
89
+ },
90
+ '&.left': {
91
+ left: 0,
92
+ right: 'auto',
93
+ paddingRight: 0,
94
+ borderTopLeftRadius: theme.shape.borderRadius,
95
+ borderBottomLeftRadius: theme.shape.borderRadius,
96
+ },
97
+ '&.right': {
98
+ right: 0,
99
+ left: 'auto',
100
+ paddingLeft: 0,
101
+ borderTopRightRadius: theme.shape.borderRadius,
102
+ borderBottomRightRadius: theme.shape.borderRadius,
103
+ }
104
+ }))
105
+
106
+ // Add new styled component for the cursor
107
+ const TimelineCursor = styled(Box)(({ theme }) => ({
108
+ position: 'absolute',
109
+ top: 0,
110
+ width: '2px',
111
+ height: '100%', // Full height of container
112
+ backgroundColor: theme.palette.error.main, // Red color
113
+ pointerEvents: 'none', // Ensure it doesn't interfere with clicks
114
+ transition: 'left 0.1s linear', // Smooth movement
115
+ zIndex: 1, // Ensure it's above other elements
116
+ }))
117
+
118
+ export default function TimelineEditor({ words, startTime, endTime, onWordUpdate, onUnsyncWord, currentTime = 0, onPlaySegment, showPlaybackIndicator = true }: TimelineEditorProps) {
119
+ const containerRef = useRef<HTMLDivElement>(null)
120
+ const [dragState, setDragState] = useState<{
121
+ wordIndex: number
122
+ type: 'move' | 'resize-left' | 'resize-right'
123
+ initialX: number
124
+ initialTime: number
125
+ word: Word
126
+ } | null>(null)
127
+
128
+ const MIN_DURATION = 0.1 // Minimum word duration in seconds
129
+
130
+ const checkCollision = (
131
+ proposedStart: number,
132
+ proposedEnd: number,
133
+ currentIndex: number,
134
+ isResize: boolean
135
+ ): boolean => {
136
+ if (isResize) {
137
+ if (currentIndex === words.length - 1) return false;
138
+
139
+ const nextWord = words[currentIndex + 1]
140
+ if (!nextWord || nextWord.start_time === null) return false
141
+ return proposedEnd > nextWord.start_time
142
+ }
143
+
144
+ return words.some((word, index) => {
145
+ if (index === currentIndex) return false
146
+ if (word.start_time === null || word.end_time === null) return false
147
+
148
+ return (
149
+ (proposedStart >= word.start_time && proposedStart <= word.end_time) ||
150
+ (proposedEnd >= word.start_time && proposedEnd <= word.end_time) ||
151
+ (proposedStart <= word.start_time && proposedEnd >= word.end_time)
152
+ )
153
+ })
154
+ }
155
+
156
+ const timeToPosition = (time: number): number => {
157
+ const duration = endTime - startTime
158
+ const position = ((time - startTime) / duration) * 100
159
+ return Math.max(0, Math.min(100, position))
160
+ }
161
+ const generateTimelineMarks = () => {
162
+ const marks = []
163
+ const startSecond = Math.floor(startTime)
164
+ const endSecond = Math.ceil(endTime)
165
+
166
+ // Generate marks for each second
167
+ for (let time = startSecond; time <= endSecond; time++) {
168
+ if (time >= startTime && time <= endTime) {
169
+ const position = timeToPosition(time)
170
+ marks.push(
171
+ <Box key={time}>
172
+ <TimelineMark sx={{ left: `${position}%` }} />
173
+ <TimelineLabel sx={{ left: `${position}%` }}>
174
+ {time}s
175
+ </TimelineLabel>
176
+ </Box>
177
+ )
178
+ }
179
+ }
180
+ return marks
181
+ }
182
+
183
+ const handleMouseDown = (e: React.MouseEvent, wordIndex: number, type: 'move' | 'resize-left' | 'resize-right') => {
184
+ const rect = containerRef.current?.getBoundingClientRect()
185
+ if (!rect) return
186
+
187
+ const word = words[wordIndex]
188
+ if (word.start_time === null || word.end_time === null) return
189
+
190
+ const initialX = e.clientX - rect.left
191
+ const initialTime = ((initialX / rect.width) * (endTime - startTime))
192
+
193
+ setDragState({
194
+ wordIndex,
195
+ type,
196
+ initialX,
197
+ initialTime,
198
+ word
199
+ })
200
+ }
201
+
202
+ const handleMouseMove = (e: React.MouseEvent) => {
203
+ if (!dragState || !containerRef.current) return
204
+
205
+ const rect = containerRef.current.getBoundingClientRect()
206
+ const x = e.clientX - rect.left
207
+ const width = rect.width
208
+
209
+ const currentWord = words[dragState.wordIndex]
210
+ if (currentWord.start_time === null || currentWord.end_time === null ||
211
+ dragState.word.start_time === null || dragState.word.end_time === null) return
212
+
213
+ if (dragState.type === 'resize-right') {
214
+ const initialWordDuration = dragState.word.end_time - dragState.word.start_time
215
+ const initialWordWidth = (initialWordDuration / (endTime - startTime)) * width
216
+ const pixelDelta = x - dragState.initialX
217
+ const percentageMoved = pixelDelta / initialWordWidth
218
+ const timeDelta = initialWordDuration * percentageMoved
219
+
220
+ const proposedEnd = Math.max(
221
+ currentWord.start_time + MIN_DURATION,
222
+ dragState.word.end_time + timeDelta
223
+ )
224
+
225
+ if (checkCollision(currentWord.start_time, proposedEnd, dragState.wordIndex, true)) return
226
+
227
+ onWordUpdate(dragState.wordIndex, {
228
+ start_time: currentWord.start_time,
229
+ end_time: proposedEnd
230
+ })
231
+ } else if (dragState.type === 'resize-left') {
232
+ const initialWordDuration = dragState.word.end_time - dragState.word.start_time
233
+ const initialWordWidth = (initialWordDuration / (endTime - startTime)) * width
234
+ const pixelDelta = x - dragState.initialX
235
+ const percentageMoved = pixelDelta / initialWordWidth
236
+ const timeDelta = initialWordDuration * percentageMoved
237
+
238
+ const proposedStart = Math.min(
239
+ currentWord.end_time - MIN_DURATION,
240
+ dragState.word.start_time + timeDelta
241
+ )
242
+
243
+ if (checkCollision(proposedStart, currentWord.end_time, dragState.wordIndex, true)) return
244
+
245
+ onWordUpdate(dragState.wordIndex, {
246
+ start_time: proposedStart,
247
+ end_time: currentWord.end_time
248
+ })
249
+ } else if (dragState.type === 'move') {
250
+ const pixelsPerSecond = width / (endTime - startTime)
251
+ const pixelDelta = x - dragState.initialX
252
+ const timeDelta = pixelDelta / pixelsPerSecond
253
+
254
+ const wordDuration = currentWord.end_time - currentWord.start_time
255
+ const proposedStart = dragState.word.start_time + timeDelta
256
+ const proposedEnd = proposedStart + wordDuration
257
+
258
+ if (proposedStart < startTime || proposedEnd > endTime) return
259
+ if (checkCollision(proposedStart, proposedEnd, dragState.wordIndex, false)) return
260
+
261
+ onWordUpdate(dragState.wordIndex, {
262
+ start_time: proposedStart,
263
+ end_time: proposedEnd
264
+ })
265
+ }
266
+ }
267
+
268
+ const handleMouseUp = () => {
269
+ setDragState(null)
270
+ }
271
+
272
+ const handleContextMenu = (e: React.MouseEvent, wordIndex: number) => {
273
+ e.preventDefault()
274
+ e.stopPropagation()
275
+
276
+ // Only unsync synced words
277
+ const word = words[wordIndex]
278
+ if (word.start_time === null || word.end_time === null) return
279
+
280
+ // Directly unsync the word without showing a menu
281
+ if (onUnsyncWord) {
282
+ console.log('TimelineEditor - Right-click unsync word', {
283
+ wordIndex,
284
+ wordText: word.text
285
+ })
286
+ onUnsyncWord(wordIndex)
287
+ }
288
+ }
289
+
290
+ const isWordHighlighted = (word: Word): boolean => {
291
+ if (!currentTime || word.start_time === null || word.end_time === null) return false
292
+ return currentTime >= word.start_time && currentTime <= word.end_time
293
+ }
294
+
295
+ const handleTimelineClick = (e: React.MouseEvent) => {
296
+ const rect = containerRef.current?.getBoundingClientRect()
297
+ if (!rect || !onPlaySegment) return
298
+
299
+ const x = e.clientX - rect.left
300
+ const clickedPosition = (x / rect.width) * (endTime - startTime) + startTime
301
+
302
+ console.log('Timeline clicked:', {
303
+ x,
304
+ width: rect.width,
305
+ clickedTime: clickedPosition
306
+ })
307
+
308
+ onPlaySegment(clickedPosition)
309
+ }
310
+
311
+ return (
312
+ <TimelineContainer
313
+ ref={containerRef}
314
+ onMouseMove={handleMouseMove}
315
+ onMouseUp={handleMouseUp}
316
+ onMouseLeave={handleMouseUp}
317
+ >
318
+ <TimelineRuler onClick={handleTimelineClick}>
319
+ {generateTimelineMarks()}
320
+ </TimelineRuler>
321
+
322
+ {/* Add cursor line */}
323
+ {showPlaybackIndicator && (
324
+ <TimelineCursor
325
+ sx={{
326
+ left: `${timeToPosition(currentTime)}%`,
327
+ display: currentTime >= startTime && currentTime <= endTime ? 'block' : 'none'
328
+ }}
329
+ />
330
+ )}
331
+
332
+ {words.map((word, index) => {
333
+ // Only render synced words on the timeline
334
+ if (word.start_time === null || word.end_time === null) return null;
335
+
336
+ const leftPosition = timeToPosition(word.start_time)
337
+ const rightPosition = timeToPosition(word.end_time)
338
+ const width = rightPosition - leftPosition
339
+ const adjustedWidth = width
340
+
341
+ return (
342
+ <TimelineWord
343
+ key={index}
344
+ className={isWordHighlighted(word) ? 'highlighted' : ''}
345
+ sx={{
346
+ left: `${leftPosition}%`,
347
+ width: `${adjustedWidth}%`,
348
+ maxWidth: `calc(${100 - leftPosition}%)`,
349
+ }}
350
+ onMouseDown={(e) => {
351
+ e.stopPropagation()
352
+ handleMouseDown(e, index, 'move')
353
+ }}
354
+ onContextMenu={(e) => handleContextMenu(e, index)}
355
+ >
356
+ <ResizeHandle
357
+ className="left"
358
+ onMouseDown={(e) => {
359
+ e.stopPropagation()
360
+ handleMouseDown(e, index, 'resize-left')
361
+ }}
362
+ />
363
+ {word.text}
364
+ <ResizeHandle
365
+ className="right"
366
+ onMouseDown={(e) => {
367
+ e.stopPropagation()
368
+ handleMouseDown(e, index, 'resize-right')
369
+ }}
370
+ />
371
+ </TimelineWord>
372
+ )
373
+ })}
374
+ </TimelineContainer>
375
+ )
376
+ }
@@ -0,0 +1,131 @@
1
+ import {
2
+ Dialog,
3
+ DialogTitle,
4
+ DialogContent,
5
+ DialogActions,
6
+ Button,
7
+ TextField,
8
+ Box,
9
+ Typography,
10
+ ButtonGroup,
11
+ IconButton,
12
+ } from '@mui/material';
13
+ import CloseIcon from '@mui/icons-material/Close';
14
+ import { useState, useEffect } from 'react';
15
+
16
+ interface TimingOffsetModalProps {
17
+ open: boolean;
18
+ onClose: () => void;
19
+ currentOffset: number;
20
+ onApply: (offsetMs: number) => void;
21
+ }
22
+
23
+ export default function TimingOffsetModal({
24
+ open,
25
+ onClose,
26
+ currentOffset,
27
+ onApply,
28
+ }: TimingOffsetModalProps) {
29
+ const [offsetMs, setOffsetMs] = useState(currentOffset);
30
+
31
+ // Reset the offset value when the modal opens
32
+ useEffect(() => {
33
+ if (open) {
34
+ setOffsetMs(currentOffset);
35
+ }
36
+ }, [open, currentOffset]);
37
+
38
+ // Handle preset buttons click
39
+ const handlePresetClick = (value: number) => {
40
+ setOffsetMs((prev) => prev + value);
41
+ };
42
+
43
+ // Handle direct input change
44
+ const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
45
+ const value = e.target.value === '' ? 0 : parseInt(e.target.value, 10);
46
+ if (!isNaN(value)) {
47
+ setOffsetMs(value);
48
+ }
49
+ };
50
+
51
+ // Apply the offset
52
+ const handleApply = () => {
53
+ onApply(offsetMs);
54
+ onClose();
55
+ };
56
+
57
+ return (
58
+ <Dialog
59
+ open={open}
60
+ onClose={onClose}
61
+ maxWidth="sm"
62
+ fullWidth
63
+ PaperProps={{
64
+ sx: {
65
+ overflowY: 'visible',
66
+ }
67
+ }}
68
+ >
69
+ <DialogTitle sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
70
+ Adjust Global Timing Offset
71
+ <IconButton onClick={onClose} size="small">
72
+ <CloseIcon />
73
+ </IconButton>
74
+ </DialogTitle>
75
+ <DialogContent>
76
+ <Box sx={{ mb: 3, mt: 1 }}>
77
+ <Typography variant="body2" sx={{ mb: 1 }}>
78
+ Adjust the timing of all words in the transcription. Positive values delay the timing, negative values advance it.
79
+ </Typography>
80
+
81
+ <Typography variant="body2" sx={{ fontStyle: 'italic', mb: 2 }}>
82
+ Note: This offset is applied globally but doesn't modify the original timestamps.
83
+ </Typography>
84
+
85
+ <Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
86
+ <Typography variant="body1" sx={{ mr: 2 }}>
87
+ Offset:
88
+ </Typography>
89
+ <TextField
90
+ value={offsetMs}
91
+ onChange={handleInputChange}
92
+ type="number"
93
+ variant="outlined"
94
+ size="small"
95
+ InputProps={{
96
+ endAdornment: <Typography variant="body2" sx={{ ml: 1 }}>ms</Typography>,
97
+ }}
98
+ sx={{ width: 120 }}
99
+ />
100
+ </Box>
101
+
102
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
103
+ <Typography variant="body2">Quick adjust:</Typography>
104
+ <Box sx={{ display: 'flex', justifyContent: 'center', gap: 1 }}>
105
+ <ButtonGroup size="small">
106
+ <Button onClick={() => handlePresetClick(-100)}>-100ms</Button>
107
+ <Button onClick={() => handlePresetClick(-50)}>-50ms</Button>
108
+ <Button onClick={() => handlePresetClick(-10)}>-10ms</Button>
109
+ </ButtonGroup>
110
+ <ButtonGroup size="small">
111
+ <Button onClick={() => handlePresetClick(10)}>+10ms</Button>
112
+ <Button onClick={() => handlePresetClick(50)}>+50ms</Button>
113
+ <Button onClick={() => handlePresetClick(100)}>+100ms</Button>
114
+ </ButtonGroup>
115
+ </Box>
116
+ </Box>
117
+ </Box>
118
+ </DialogContent>
119
+ <DialogActions>
120
+ <Button onClick={onClose}>Cancel</Button>
121
+ <Button
122
+ onClick={handleApply}
123
+ variant="contained"
124
+ color={offsetMs === 0 ? "warning" : "primary"}
125
+ >
126
+ {offsetMs === 0 ? "Remove Offset" : "Apply Offset"}
127
+ </Button>
128
+ </DialogActions>
129
+ </Dialog>
130
+ );
131
+ }