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,496 @@
1
+ import {
2
+ Box,
3
+ Button,
4
+ Typography,
5
+ IconButton,
6
+ Tooltip,
7
+ Stack
8
+ } from '@mui/material'
9
+ import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline'
10
+ import CancelIcon from '@mui/icons-material/Cancel'
11
+ import ZoomInIcon from '@mui/icons-material/ZoomIn'
12
+ import ZoomOutIcon from '@mui/icons-material/ZoomOut'
13
+ import ArrowBackIcon from '@mui/icons-material/ArrowBack'
14
+ import ArrowForwardIcon from '@mui/icons-material/ArrowForward'
15
+ import AutorenewIcon from '@mui/icons-material/Autorenew'
16
+ import PauseCircleOutlineIcon from '@mui/icons-material/PauseCircleOutline'
17
+ import PlayArrowIcon from '@mui/icons-material/PlayArrow'
18
+ import StopIcon from '@mui/icons-material/Stop'
19
+ import CenterFocusStrongIcon from '@mui/icons-material/CenterFocusStrong'
20
+ import TimelineEditor from './TimelineEditor'
21
+ import { Word } from '../types'
22
+ import { useState, useEffect, useCallback, useRef, useMemo, memo } from 'react'
23
+
24
+ interface EditTimelineSectionProps {
25
+ words: Word[]
26
+ startTime: number
27
+ endTime: number
28
+ originalStartTime: number | null
29
+ originalEndTime: number | null
30
+ currentStartTime: number | null
31
+ currentEndTime: number | null
32
+ currentTime?: number
33
+ isManualSyncing: boolean
34
+ syncWordIndex: number
35
+ isSpacebarPressed: boolean
36
+ onWordUpdate: (index: number, updates: Partial<Word>) => void
37
+ onUnsyncWord?: (index: number) => void
38
+ onPlaySegment?: (time: number) => void
39
+ onStopAudio?: () => void
40
+ startManualSync: () => void
41
+ pauseManualSync?: () => void
42
+ resumeManualSync?: () => void
43
+ isPaused?: boolean
44
+ isGlobal?: boolean
45
+ defaultZoomLevel?: number
46
+ isReplaceAllMode?: boolean
47
+ }
48
+
49
+ // Memoized control buttons to prevent unnecessary re-renders
50
+ const TimelineControls = memo(({
51
+ isGlobal,
52
+ visibleStartTime,
53
+ visibleEndTime,
54
+ startTime,
55
+ endTime,
56
+ zoomLevel,
57
+ autoScrollEnabled,
58
+ currentTime,
59
+ isManualSyncing,
60
+ isReplaceAllMode,
61
+ isPaused,
62
+ onScrollLeft,
63
+ onZoomOut,
64
+ onZoomIn,
65
+ onScrollRight,
66
+ onToggleAutoScroll,
67
+ onJumpToCurrentTime,
68
+ onStartManualSync,
69
+ onPauseResume,
70
+ onStopAudio
71
+ }: {
72
+ isGlobal: boolean
73
+ visibleStartTime: number
74
+ visibleEndTime: number
75
+ startTime: number
76
+ endTime: number
77
+ zoomLevel: number
78
+ autoScrollEnabled: boolean
79
+ currentTime?: number
80
+ isManualSyncing: boolean
81
+ isReplaceAllMode: boolean
82
+ isPaused: boolean
83
+ onScrollLeft: () => void
84
+ onZoomOut: () => void
85
+ onZoomIn: () => void
86
+ onScrollRight: () => void
87
+ onToggleAutoScroll: () => void
88
+ onJumpToCurrentTime: () => void
89
+ onStartManualSync: () => void
90
+ onPauseResume: () => void
91
+ onStopAudio?: () => void
92
+ }) => {
93
+ return (
94
+ <Stack direction="row" spacing={1} alignItems="center">
95
+ {isGlobal && (
96
+ <>
97
+ <Tooltip title="Scroll Left">
98
+ <IconButton
99
+ onClick={onScrollLeft}
100
+ disabled={visibleStartTime <= startTime}
101
+ size="small"
102
+ >
103
+ <ArrowBackIcon />
104
+ </IconButton>
105
+ </Tooltip>
106
+ <Tooltip title="Zoom Out (Show More Time)">
107
+ <IconButton
108
+ onClick={onZoomOut}
109
+ disabled={zoomLevel >= (endTime - startTime) || (isReplaceAllMode && isManualSyncing && !isPaused)}
110
+ size="small"
111
+ >
112
+ <ZoomOutIcon />
113
+ </IconButton>
114
+ </Tooltip>
115
+ <Tooltip title="Zoom In (Show Less Time)">
116
+ <IconButton
117
+ onClick={onZoomIn}
118
+ disabled={zoomLevel <= 2 || (isReplaceAllMode && isManualSyncing && !isPaused)}
119
+ size="small"
120
+ >
121
+ <ZoomInIcon />
122
+ </IconButton>
123
+ </Tooltip>
124
+ <Tooltip title="Scroll Right">
125
+ <IconButton
126
+ onClick={onScrollRight}
127
+ disabled={visibleEndTime >= endTime}
128
+ size="small"
129
+ >
130
+ <ArrowForwardIcon />
131
+ </IconButton>
132
+ </Tooltip>
133
+ <Tooltip
134
+ title={autoScrollEnabled ?
135
+ "Disable Auto-Page Turn During Playback" :
136
+ "Enable Auto-Page Turn During Playback"}
137
+ >
138
+ <IconButton
139
+ onClick={onToggleAutoScroll}
140
+ color={autoScrollEnabled ? "primary" : "default"}
141
+ size="small"
142
+ >
143
+ {autoScrollEnabled ? <AutorenewIcon /> : <PauseCircleOutlineIcon />}
144
+ </IconButton>
145
+ </Tooltip>
146
+ <Tooltip title="Jump to Current Playback Position">
147
+ <IconButton
148
+ onClick={onJumpToCurrentTime}
149
+ disabled={!currentTime}
150
+ size="small"
151
+ >
152
+ <CenterFocusStrongIcon />
153
+ </IconButton>
154
+ </Tooltip>
155
+ </>
156
+ )}
157
+ {isReplaceAllMode && onStopAudio && (
158
+ <Button
159
+ variant="outlined"
160
+ onClick={onStopAudio}
161
+ startIcon={<StopIcon />}
162
+ color="error"
163
+ size="small"
164
+ >
165
+ Stop Audio
166
+ </Button>
167
+ )}
168
+ <Button
169
+ variant={isManualSyncing ? "outlined" : "contained"}
170
+ onClick={onStartManualSync}
171
+ startIcon={isManualSyncing ? <CancelIcon /> : <PlayCircleOutlineIcon />}
172
+ color={isManualSyncing ? "error" : "primary"}
173
+ >
174
+ {isManualSyncing ? "Cancel Sync" : "Manual Sync"}
175
+ </Button>
176
+ {isManualSyncing && isReplaceAllMode && (
177
+ <Button
178
+ variant="outlined"
179
+ onClick={onPauseResume}
180
+ startIcon={isPaused ? <PlayArrowIcon /> : <PauseCircleOutlineIcon />}
181
+ color={isPaused ? "success" : "warning"}
182
+ size="small"
183
+ >
184
+ {isPaused ? "Resume" : "Pause"}
185
+ </Button>
186
+ )}
187
+ </Stack>
188
+ )
189
+ })
190
+
191
+ export default function EditTimelineSection({
192
+ words,
193
+ startTime,
194
+ endTime,
195
+ originalStartTime,
196
+ originalEndTime,
197
+ currentStartTime,
198
+ currentEndTime,
199
+ currentTime,
200
+ isManualSyncing,
201
+ syncWordIndex,
202
+ isSpacebarPressed,
203
+ onWordUpdate,
204
+ onUnsyncWord,
205
+ onPlaySegment,
206
+ onStopAudio,
207
+ startManualSync,
208
+ pauseManualSync,
209
+ resumeManualSync,
210
+ isPaused = false,
211
+ isGlobal = false,
212
+ defaultZoomLevel = 10,
213
+ isReplaceAllMode = false
214
+ }: EditTimelineSectionProps) {
215
+ // Add state for zoom level - use larger default for Replace All mode
216
+ const [zoomLevel, setZoomLevel] = useState(defaultZoomLevel)
217
+ const [visibleStartTime, setVisibleStartTime] = useState(startTime)
218
+ const [visibleEndTime, setVisibleEndTime] = useState(Math.min(startTime + zoomLevel, endTime))
219
+ const [autoScrollEnabled, setAutoScrollEnabled] = useState(true) // Default to enabled
220
+ const timelineRef = useRef<HTMLDivElement>(null)
221
+
222
+ // Memoize the effective time range to prevent recalculation
223
+ const effectiveTimeRange = useMemo(() => ({
224
+ start: isGlobal ? visibleStartTime : startTime,
225
+ end: isGlobal ? visibleEndTime : endTime
226
+ }), [isGlobal, visibleStartTime, visibleEndTime, startTime, endTime])
227
+
228
+ // Auto-enable auto-scroll when manual sync starts or resumes
229
+ useEffect(() => {
230
+ if (isManualSyncing && !isPaused) {
231
+ console.log('EditTimelineSection - Auto-enabling auto-scroll for manual sync')
232
+ setAutoScrollEnabled(true)
233
+ }
234
+ }, [isManualSyncing, isPaused])
235
+
236
+ // Initial setup of visible time range
237
+ useEffect(() => {
238
+ if (isGlobal) {
239
+ // For global mode, start at the beginning
240
+ setVisibleStartTime(startTime)
241
+ setVisibleEndTime(Math.min(startTime + zoomLevel, endTime))
242
+ } else {
243
+ // For segment mode, always show the full segment
244
+ setVisibleStartTime(startTime)
245
+ setVisibleEndTime(endTime)
246
+ }
247
+ }, [startTime, endTime, zoomLevel, isGlobal])
248
+
249
+ // Throttled auto-scroll to reduce frequent updates during playback
250
+ const lastScrollUpdateRef = useRef<number>(0)
251
+ const SCROLL_THROTTLE_MS = 100 // Only update scroll position every 100ms
252
+
253
+ // Handle playback scrolling with "page turning" approach - throttled for performance
254
+ useEffect(() => {
255
+ // Skip if not in global mode, no current time, or auto-scroll is disabled
256
+ if (!isGlobal || !currentTime || !autoScrollEnabled) return
257
+
258
+ // Throttle scroll updates for performance
259
+ const now = Date.now()
260
+ if (now - lastScrollUpdateRef.current < SCROLL_THROTTLE_MS) return
261
+ lastScrollUpdateRef.current = now
262
+
263
+ // Only scroll when current time is outside or near the edge of the visible window
264
+ if (currentTime < visibleStartTime) {
265
+ // If current time is before visible window, jump to show it at the start
266
+ const newStart = Math.max(startTime, currentTime)
267
+ const newEnd = Math.min(endTime, newStart + zoomLevel)
268
+ setVisibleStartTime(newStart)
269
+ setVisibleEndTime(newEnd)
270
+ } else if (currentTime > visibleEndTime - (zoomLevel * 0.05)) {
271
+ // If current time is near the end of visible window (within 5% of zoom level from the end),
272
+ // jump to the next "page" with current time at 5% from the left
273
+ const pageOffset = zoomLevel * 0.05 // Position current time 5% from the left edge
274
+ const newStart = Math.max(startTime, currentTime - pageOffset)
275
+ const newEnd = Math.min(endTime, newStart + zoomLevel)
276
+
277
+ // Only update if we're actually moving forward
278
+ if (newStart > visibleStartTime) {
279
+ setVisibleStartTime(newStart)
280
+ setVisibleEndTime(newEnd)
281
+ }
282
+ }
283
+ }, [currentTime, visibleStartTime, visibleEndTime, startTime, endTime, zoomLevel, isGlobal, autoScrollEnabled])
284
+
285
+ // Update visible time range when zoom level changes - but don't auto-center on current time
286
+ useEffect(() => {
287
+ if (isGlobal) {
288
+ // Don't auto-center on current time, just adjust the visible window based on zoom level
289
+ // while keeping the left edge fixed (unless it would go out of bounds)
290
+ const newEnd = Math.min(endTime, visibleStartTime + zoomLevel)
291
+
292
+ // If the new end would exceed the total range, adjust the start time
293
+ if (newEnd === endTime) {
294
+ const newStart = Math.max(startTime, endTime - zoomLevel)
295
+ setVisibleStartTime(newStart)
296
+ }
297
+
298
+ setVisibleEndTime(newEnd)
299
+ } else {
300
+ // For segment mode, always show the full segment
301
+ setVisibleStartTime(startTime)
302
+ setVisibleEndTime(endTime)
303
+ }
304
+ }, [zoomLevel, startTime, endTime, isGlobal, visibleStartTime])
305
+
306
+ // Memoized event handlers to prevent unnecessary re-renders
307
+ const handleZoomIn = useCallback(() => {
308
+ if (isReplaceAllMode && isManualSyncing && !isPaused) return // Prevent zoom changes during active sync (but allow when paused)
309
+ if (zoomLevel > 2) { // Minimum zoom level of 2 seconds
310
+ setZoomLevel(zoomLevel - 2)
311
+ }
312
+ }, [isReplaceAllMode, isManualSyncing, isPaused, zoomLevel])
313
+
314
+ const handleZoomOut = useCallback(() => {
315
+ if (isReplaceAllMode && isManualSyncing && !isPaused) return // Prevent zoom changes during active sync (but allow when paused)
316
+ if (zoomLevel < (endTime - startTime)) { // Maximum zoom is the full range
317
+ setZoomLevel(zoomLevel + 2)
318
+ }
319
+ }, [isReplaceAllMode, isManualSyncing, isPaused, zoomLevel, endTime, startTime])
320
+
321
+ const toggleAutoScroll = useCallback(() => {
322
+ setAutoScrollEnabled(!autoScrollEnabled)
323
+ }, [autoScrollEnabled])
324
+
325
+ const jumpToCurrentTime = useCallback(() => {
326
+ if (!isGlobal || !currentTime) return
327
+
328
+ // Center the view around the current time
329
+ const halfZoom = zoomLevel / 2
330
+ let newStart = Math.max(startTime, currentTime - halfZoom)
331
+ const newEnd = Math.min(endTime, newStart + zoomLevel)
332
+
333
+ // Adjust start time if end time hits the boundary
334
+ if (newEnd === endTime) {
335
+ newStart = Math.max(startTime, endTime - zoomLevel)
336
+ }
337
+
338
+ setVisibleStartTime(newStart)
339
+ setVisibleEndTime(newEnd)
340
+ }, [currentTime, zoomLevel, startTime, endTime, isGlobal])
341
+
342
+ // Handle horizontal scrolling - throttled for performance
343
+ const handleScroll = useCallback((event: React.WheelEvent<HTMLDivElement>) => {
344
+ if (isGlobal && event.deltaX !== 0) {
345
+ event.preventDefault()
346
+
347
+ // Disable auto-scroll when user manually scrolls
348
+ setAutoScrollEnabled(false)
349
+
350
+ // Calculate scroll amount in seconds (scale based on zoom level)
351
+ const scrollAmount = (event.deltaX / 100) * (zoomLevel / 10)
352
+
353
+ // Update visible time range
354
+ let newStart = visibleStartTime + scrollAmount
355
+ let newEnd = visibleEndTime + scrollAmount
356
+
357
+ // Ensure we don't scroll beyond the boundaries
358
+ if (newStart < startTime) {
359
+ newStart = startTime
360
+ newEnd = newStart + zoomLevel
361
+ }
362
+
363
+ if (newEnd > endTime) {
364
+ newEnd = endTime
365
+ newStart = Math.max(startTime, newEnd - zoomLevel)
366
+ }
367
+
368
+ setVisibleStartTime(newStart)
369
+ setVisibleEndTime(newEnd)
370
+ }
371
+ }, [isGlobal, visibleStartTime, visibleEndTime, startTime, endTime, zoomLevel])
372
+
373
+ const handleScrollLeft = useCallback(() => {
374
+ if (!isGlobal) return
375
+
376
+ // Disable auto-scroll when user manually scrolls
377
+ setAutoScrollEnabled(false)
378
+
379
+ // Scroll left by 25% of the current visible range
380
+ const scrollAmount = zoomLevel * 0.25
381
+ const newStart = Math.max(startTime, visibleStartTime - scrollAmount)
382
+ const newEnd = newStart + zoomLevel
383
+
384
+ setVisibleStartTime(newStart)
385
+ setVisibleEndTime(newEnd)
386
+ }, [isGlobal, zoomLevel, startTime, visibleStartTime])
387
+
388
+ const handleScrollRight = useCallback(() => {
389
+ if (!isGlobal) return
390
+
391
+ // Disable auto-scroll when user manually scrolls
392
+ setAutoScrollEnabled(false)
393
+
394
+ // Scroll right by 25% of the current visible range
395
+ const scrollAmount = zoomLevel * 0.25
396
+ const newEnd = Math.min(endTime, visibleEndTime + scrollAmount)
397
+ let newStart = newEnd - zoomLevel
398
+
399
+ // Ensure we don't scroll beyond the start boundary
400
+ if (newStart < startTime) {
401
+ newStart = startTime
402
+ const adjustedNewEnd = Math.min(endTime, newStart + zoomLevel)
403
+ setVisibleEndTime(adjustedNewEnd)
404
+ } else {
405
+ setVisibleEndTime(newEnd)
406
+ }
407
+
408
+ setVisibleStartTime(newStart)
409
+ }, [isGlobal, zoomLevel, endTime, visibleEndTime, startTime])
410
+
411
+ const handlePauseResume = useCallback(() => {
412
+ if (isPaused && resumeManualSync) {
413
+ resumeManualSync()
414
+ } else if (!isPaused && pauseManualSync) {
415
+ pauseManualSync()
416
+ }
417
+ }, [isPaused, resumeManualSync, pauseManualSync])
418
+
419
+ // Memoize current word info to prevent recalculation
420
+ const currentWordInfo = useMemo(() => {
421
+ if (!isManualSyncing || syncWordIndex < 0 || syncWordIndex >= words.length) {
422
+ return null
423
+ }
424
+
425
+ return {
426
+ index: syncWordIndex + 1,
427
+ total: words.length,
428
+ text: words[syncWordIndex]?.text || ''
429
+ }
430
+ }, [isManualSyncing, syncWordIndex, words])
431
+
432
+ return (
433
+ <>
434
+ <Box
435
+ sx={{ height: '120px', mb: 2 }}
436
+ ref={timelineRef}
437
+ onWheel={handleScroll}
438
+ >
439
+ <TimelineEditor
440
+ words={words}
441
+ startTime={effectiveTimeRange.start}
442
+ endTime={effectiveTimeRange.end}
443
+ onWordUpdate={onWordUpdate}
444
+ currentTime={currentTime}
445
+ onPlaySegment={onPlaySegment}
446
+ onUnsyncWord={onUnsyncWord}
447
+ />
448
+ </Box>
449
+
450
+ <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
451
+ <Typography variant="body2" color="text.secondary">
452
+ Original Time Range: {originalStartTime?.toFixed(2) ?? 'N/A'} - {originalEndTime?.toFixed(2) ?? 'N/A'}
453
+ <br />
454
+ Current Time Range: {currentStartTime?.toFixed(2) ?? 'N/A'} - {currentEndTime?.toFixed(2) ?? 'N/A'}
455
+ </Typography>
456
+
457
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
458
+ <TimelineControls
459
+ isGlobal={isGlobal}
460
+ visibleStartTime={visibleStartTime}
461
+ visibleEndTime={visibleEndTime}
462
+ startTime={startTime}
463
+ endTime={endTime}
464
+ zoomLevel={zoomLevel}
465
+ autoScrollEnabled={autoScrollEnabled}
466
+ currentTime={currentTime}
467
+ isManualSyncing={isManualSyncing}
468
+ isReplaceAllMode={isReplaceAllMode}
469
+ isPaused={isPaused}
470
+ onScrollLeft={handleScrollLeft}
471
+ onZoomOut={handleZoomOut}
472
+ onZoomIn={handleZoomIn}
473
+ onScrollRight={handleScrollRight}
474
+ onToggleAutoScroll={toggleAutoScroll}
475
+ onJumpToCurrentTime={jumpToCurrentTime}
476
+ onStartManualSync={startManualSync}
477
+ onPauseResume={handlePauseResume}
478
+ onStopAudio={onStopAudio}
479
+ />
480
+ {currentWordInfo && (
481
+ <Box>
482
+ <Typography variant="body2">
483
+ Word {currentWordInfo.index} of {currentWordInfo.total}: <strong>{currentWordInfo.text}</strong>
484
+ </Typography>
485
+ <Typography variant="caption" color="text.secondary">
486
+ {isSpacebarPressed ?
487
+ "Holding spacebar... Release when word ends" :
488
+ "Press spacebar when word starts (tap for short words, hold for long words)"}
489
+ </Typography>
490
+ </Box>
491
+ )}
492
+ </Box>
493
+ </Box>
494
+ </>
495
+ )
496
+ }