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.
- karaoke_gen/__init__.py +38 -0
- karaoke_gen/audio_fetcher.py +1614 -0
- karaoke_gen/audio_processor.py +790 -0
- karaoke_gen/config.py +83 -0
- karaoke_gen/file_handler.py +387 -0
- karaoke_gen/instrumental_review/__init__.py +45 -0
- karaoke_gen/instrumental_review/analyzer.py +408 -0
- karaoke_gen/instrumental_review/editor.py +322 -0
- karaoke_gen/instrumental_review/models.py +171 -0
- karaoke_gen/instrumental_review/server.py +475 -0
- karaoke_gen/instrumental_review/static/index.html +1529 -0
- karaoke_gen/instrumental_review/waveform.py +409 -0
- karaoke_gen/karaoke_finalise/__init__.py +1 -0
- karaoke_gen/karaoke_finalise/karaoke_finalise.py +1833 -0
- karaoke_gen/karaoke_gen.py +1026 -0
- karaoke_gen/lyrics_processor.py +474 -0
- karaoke_gen/metadata.py +160 -0
- karaoke_gen/pipeline/__init__.py +87 -0
- karaoke_gen/pipeline/base.py +215 -0
- karaoke_gen/pipeline/context.py +230 -0
- karaoke_gen/pipeline/executors/__init__.py +21 -0
- karaoke_gen/pipeline/executors/local.py +159 -0
- karaoke_gen/pipeline/executors/remote.py +257 -0
- karaoke_gen/pipeline/stages/__init__.py +27 -0
- karaoke_gen/pipeline/stages/finalize.py +202 -0
- karaoke_gen/pipeline/stages/render.py +165 -0
- karaoke_gen/pipeline/stages/screens.py +139 -0
- karaoke_gen/pipeline/stages/separation.py +191 -0
- karaoke_gen/pipeline/stages/transcription.py +191 -0
- karaoke_gen/resources/AvenirNext-Bold.ttf +0 -0
- karaoke_gen/resources/Montserrat-Bold.ttf +0 -0
- karaoke_gen/resources/Oswald-Bold.ttf +0 -0
- karaoke_gen/resources/Oswald-SemiBold.ttf +0 -0
- karaoke_gen/resources/Zurich_Cn_BT_Bold.ttf +0 -0
- karaoke_gen/style_loader.py +531 -0
- karaoke_gen/utils/__init__.py +18 -0
- karaoke_gen/utils/bulk_cli.py +492 -0
- karaoke_gen/utils/cli_args.py +432 -0
- karaoke_gen/utils/gen_cli.py +978 -0
- karaoke_gen/utils/remote_cli.py +3268 -0
- karaoke_gen/video_background_processor.py +351 -0
- karaoke_gen/video_generator.py +424 -0
- karaoke_gen-0.75.54.dist-info/METADATA +718 -0
- karaoke_gen-0.75.54.dist-info/RECORD +287 -0
- karaoke_gen-0.75.54.dist-info/WHEEL +4 -0
- karaoke_gen-0.75.54.dist-info/entry_points.txt +5 -0
- karaoke_gen-0.75.54.dist-info/licenses/LICENSE +21 -0
- lyrics_transcriber/__init__.py +10 -0
- lyrics_transcriber/cli/__init__.py +0 -0
- lyrics_transcriber/cli/cli_main.py +285 -0
- lyrics_transcriber/core/__init__.py +0 -0
- lyrics_transcriber/core/config.py +50 -0
- lyrics_transcriber/core/controller.py +594 -0
- lyrics_transcriber/correction/__init__.py +0 -0
- lyrics_transcriber/correction/agentic/__init__.py +9 -0
- lyrics_transcriber/correction/agentic/adapter.py +71 -0
- lyrics_transcriber/correction/agentic/agent.py +313 -0
- lyrics_transcriber/correction/agentic/feedback/aggregator.py +12 -0
- lyrics_transcriber/correction/agentic/feedback/collector.py +17 -0
- lyrics_transcriber/correction/agentic/feedback/retention.py +24 -0
- lyrics_transcriber/correction/agentic/feedback/store.py +76 -0
- lyrics_transcriber/correction/agentic/handlers/__init__.py +24 -0
- lyrics_transcriber/correction/agentic/handlers/ambiguous.py +44 -0
- lyrics_transcriber/correction/agentic/handlers/background_vocals.py +68 -0
- lyrics_transcriber/correction/agentic/handlers/base.py +51 -0
- lyrics_transcriber/correction/agentic/handlers/complex_multi_error.py +46 -0
- lyrics_transcriber/correction/agentic/handlers/extra_words.py +74 -0
- lyrics_transcriber/correction/agentic/handlers/no_error.py +42 -0
- lyrics_transcriber/correction/agentic/handlers/punctuation.py +44 -0
- lyrics_transcriber/correction/agentic/handlers/registry.py +60 -0
- lyrics_transcriber/correction/agentic/handlers/repeated_section.py +44 -0
- lyrics_transcriber/correction/agentic/handlers/sound_alike.py +126 -0
- lyrics_transcriber/correction/agentic/models/__init__.py +5 -0
- lyrics_transcriber/correction/agentic/models/ai_correction.py +31 -0
- lyrics_transcriber/correction/agentic/models/correction_session.py +30 -0
- lyrics_transcriber/correction/agentic/models/enums.py +38 -0
- lyrics_transcriber/correction/agentic/models/human_feedback.py +30 -0
- lyrics_transcriber/correction/agentic/models/learning_data.py +26 -0
- lyrics_transcriber/correction/agentic/models/observability_metrics.py +28 -0
- lyrics_transcriber/correction/agentic/models/schemas.py +46 -0
- lyrics_transcriber/correction/agentic/models/utils.py +19 -0
- lyrics_transcriber/correction/agentic/observability/__init__.py +5 -0
- lyrics_transcriber/correction/agentic/observability/langfuse_integration.py +35 -0
- lyrics_transcriber/correction/agentic/observability/metrics.py +46 -0
- lyrics_transcriber/correction/agentic/observability/performance.py +19 -0
- lyrics_transcriber/correction/agentic/prompts/__init__.py +2 -0
- lyrics_transcriber/correction/agentic/prompts/classifier.py +227 -0
- lyrics_transcriber/correction/agentic/providers/__init__.py +6 -0
- lyrics_transcriber/correction/agentic/providers/base.py +36 -0
- lyrics_transcriber/correction/agentic/providers/circuit_breaker.py +145 -0
- lyrics_transcriber/correction/agentic/providers/config.py +73 -0
- lyrics_transcriber/correction/agentic/providers/constants.py +24 -0
- lyrics_transcriber/correction/agentic/providers/health.py +28 -0
- lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +212 -0
- lyrics_transcriber/correction/agentic/providers/model_factory.py +209 -0
- lyrics_transcriber/correction/agentic/providers/response_cache.py +218 -0
- lyrics_transcriber/correction/agentic/providers/response_parser.py +111 -0
- lyrics_transcriber/correction/agentic/providers/retry_executor.py +127 -0
- lyrics_transcriber/correction/agentic/router.py +35 -0
- lyrics_transcriber/correction/agentic/workflows/__init__.py +5 -0
- lyrics_transcriber/correction/agentic/workflows/consensus_workflow.py +24 -0
- lyrics_transcriber/correction/agentic/workflows/correction_graph.py +59 -0
- lyrics_transcriber/correction/agentic/workflows/feedback_workflow.py +24 -0
- lyrics_transcriber/correction/anchor_sequence.py +919 -0
- lyrics_transcriber/correction/corrector.py +760 -0
- lyrics_transcriber/correction/feedback/__init__.py +2 -0
- lyrics_transcriber/correction/feedback/schemas.py +107 -0
- lyrics_transcriber/correction/feedback/store.py +236 -0
- lyrics_transcriber/correction/handlers/__init__.py +0 -0
- lyrics_transcriber/correction/handlers/base.py +52 -0
- lyrics_transcriber/correction/handlers/extend_anchor.py +149 -0
- lyrics_transcriber/correction/handlers/levenshtein.py +189 -0
- lyrics_transcriber/correction/handlers/llm.py +293 -0
- lyrics_transcriber/correction/handlers/llm_providers.py +60 -0
- lyrics_transcriber/correction/handlers/no_space_punct_match.py +154 -0
- lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +85 -0
- lyrics_transcriber/correction/handlers/repeat.py +88 -0
- lyrics_transcriber/correction/handlers/sound_alike.py +259 -0
- lyrics_transcriber/correction/handlers/syllables_match.py +252 -0
- lyrics_transcriber/correction/handlers/word_count_match.py +80 -0
- lyrics_transcriber/correction/handlers/word_operations.py +187 -0
- lyrics_transcriber/correction/operations.py +352 -0
- lyrics_transcriber/correction/phrase_analyzer.py +435 -0
- lyrics_transcriber/correction/text_utils.py +30 -0
- lyrics_transcriber/frontend/.gitignore +23 -0
- lyrics_transcriber/frontend/.yarn/releases/yarn-4.7.0.cjs +935 -0
- lyrics_transcriber/frontend/.yarnrc.yml +3 -0
- lyrics_transcriber/frontend/README.md +50 -0
- lyrics_transcriber/frontend/REPLACE_ALL_FUNCTIONALITY.md +210 -0
- lyrics_transcriber/frontend/__init__.py +25 -0
- lyrics_transcriber/frontend/eslint.config.js +28 -0
- lyrics_transcriber/frontend/index.html +18 -0
- lyrics_transcriber/frontend/package.json +42 -0
- lyrics_transcriber/frontend/public/android-chrome-192x192.png +0 -0
- lyrics_transcriber/frontend/public/android-chrome-512x512.png +0 -0
- lyrics_transcriber/frontend/public/apple-touch-icon.png +0 -0
- lyrics_transcriber/frontend/public/favicon-16x16.png +0 -0
- lyrics_transcriber/frontend/public/favicon-32x32.png +0 -0
- lyrics_transcriber/frontend/public/favicon.ico +0 -0
- lyrics_transcriber/frontend/public/nomad-karaoke-logo.png +0 -0
- lyrics_transcriber/frontend/src/App.tsx +214 -0
- lyrics_transcriber/frontend/src/api.ts +254 -0
- lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +77 -0
- lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx +114 -0
- lyrics_transcriber/frontend/src/components/AgenticCorrectionMetrics.tsx +204 -0
- lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +180 -0
- lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +167 -0
- lyrics_transcriber/frontend/src/components/CorrectionAnnotationModal.tsx +359 -0
- lyrics_transcriber/frontend/src/components/CorrectionDetailCard.tsx +281 -0
- lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +162 -0
- lyrics_transcriber/frontend/src/components/DurationTimelineView.tsx +257 -0
- lyrics_transcriber/frontend/src/components/EditActionBar.tsx +68 -0
- lyrics_transcriber/frontend/src/components/EditModal.tsx +702 -0
- lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +496 -0
- lyrics_transcriber/frontend/src/components/EditWordList.tsx +379 -0
- lyrics_transcriber/frontend/src/components/FileUpload.tsx +77 -0
- lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +467 -0
- lyrics_transcriber/frontend/src/components/Header.tsx +413 -0
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +1387 -0
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +185 -0
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +704 -0
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/UpcomingWordsBar.tsx +80 -0
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +905 -0
- lyrics_transcriber/frontend/src/components/MetricsDashboard.tsx +51 -0
- lyrics_transcriber/frontend/src/components/ModeSelectionModal.tsx +127 -0
- lyrics_transcriber/frontend/src/components/ModeSelector.tsx +67 -0
- lyrics_transcriber/frontend/src/components/ModelSelector.tsx +23 -0
- lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +144 -0
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +268 -0
- lyrics_transcriber/frontend/src/components/ReplaceAllLyricsModal.tsx +336 -0
- lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +354 -0
- lyrics_transcriber/frontend/src/components/SegmentDetailsModal.tsx +64 -0
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +376 -0
- lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +131 -0
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +256 -0
- lyrics_transcriber/frontend/src/components/WordDivider.tsx +187 -0
- lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +379 -0
- lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +56 -0
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +87 -0
- lyrics_transcriber/frontend/src/components/shared/constants.ts +20 -0
- lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +180 -0
- lyrics_transcriber/frontend/src/components/shared/styles.ts +13 -0
- lyrics_transcriber/frontend/src/components/shared/types.js +2 -0
- lyrics_transcriber/frontend/src/components/shared/types.ts +129 -0
- lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +177 -0
- lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +78 -0
- lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +75 -0
- lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +360 -0
- lyrics_transcriber/frontend/src/components/shared/utils/timingUtils.ts +110 -0
- lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +22 -0
- lyrics_transcriber/frontend/src/hooks/useManualSync.ts +435 -0
- lyrics_transcriber/frontend/src/main.tsx +17 -0
- lyrics_transcriber/frontend/src/theme.ts +177 -0
- lyrics_transcriber/frontend/src/types/global.d.ts +9 -0
- lyrics_transcriber/frontend/src/types.js +2 -0
- lyrics_transcriber/frontend/src/types.ts +199 -0
- lyrics_transcriber/frontend/src/validation.ts +132 -0
- lyrics_transcriber/frontend/src/vite-env.d.ts +1 -0
- lyrics_transcriber/frontend/tsconfig.app.json +26 -0
- lyrics_transcriber/frontend/tsconfig.json +25 -0
- lyrics_transcriber/frontend/tsconfig.node.json +23 -0
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -0
- lyrics_transcriber/frontend/update_version.js +11 -0
- lyrics_transcriber/frontend/vite.config.d.ts +2 -0
- lyrics_transcriber/frontend/vite.config.js +10 -0
- lyrics_transcriber/frontend/vite.config.ts +11 -0
- lyrics_transcriber/frontend/web_assets/android-chrome-192x192.png +0 -0
- lyrics_transcriber/frontend/web_assets/android-chrome-512x512.png +0 -0
- lyrics_transcriber/frontend/web_assets/apple-touch-icon.png +0 -0
- lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js +43288 -0
- lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js.map +1 -0
- lyrics_transcriber/frontend/web_assets/favicon-16x16.png +0 -0
- lyrics_transcriber/frontend/web_assets/favicon-32x32.png +0 -0
- lyrics_transcriber/frontend/web_assets/favicon.ico +0 -0
- lyrics_transcriber/frontend/web_assets/index.html +18 -0
- lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.png +0 -0
- lyrics_transcriber/frontend/yarn.lock +3752 -0
- lyrics_transcriber/lyrics/__init__.py +0 -0
- lyrics_transcriber/lyrics/base_lyrics_provider.py +211 -0
- lyrics_transcriber/lyrics/file_provider.py +95 -0
- lyrics_transcriber/lyrics/genius.py +384 -0
- lyrics_transcriber/lyrics/lrclib.py +231 -0
- lyrics_transcriber/lyrics/musixmatch.py +156 -0
- lyrics_transcriber/lyrics/spotify.py +290 -0
- lyrics_transcriber/lyrics/user_input_provider.py +44 -0
- lyrics_transcriber/output/__init__.py +0 -0
- lyrics_transcriber/output/ass/__init__.py +21 -0
- lyrics_transcriber/output/ass/ass.py +2088 -0
- lyrics_transcriber/output/ass/ass_specs.txt +732 -0
- lyrics_transcriber/output/ass/config.py +180 -0
- lyrics_transcriber/output/ass/constants.py +23 -0
- lyrics_transcriber/output/ass/event.py +94 -0
- lyrics_transcriber/output/ass/formatters.py +132 -0
- lyrics_transcriber/output/ass/lyrics_line.py +265 -0
- lyrics_transcriber/output/ass/lyrics_screen.py +252 -0
- lyrics_transcriber/output/ass/section_detector.py +89 -0
- lyrics_transcriber/output/ass/section_screen.py +106 -0
- lyrics_transcriber/output/ass/style.py +187 -0
- lyrics_transcriber/output/cdg.py +619 -0
- lyrics_transcriber/output/cdgmaker/__init__.py +0 -0
- lyrics_transcriber/output/cdgmaker/cdg.py +262 -0
- lyrics_transcriber/output/cdgmaker/composer.py +2260 -0
- lyrics_transcriber/output/cdgmaker/config.py +151 -0
- lyrics_transcriber/output/cdgmaker/images/instrumental.png +0 -0
- lyrics_transcriber/output/cdgmaker/images/intro.png +0 -0
- lyrics_transcriber/output/cdgmaker/pack.py +507 -0
- lyrics_transcriber/output/cdgmaker/render.py +346 -0
- lyrics_transcriber/output/cdgmaker/transitions/centertexttoplogobottomtext.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/circlein.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/circleout.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/fizzle.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/largecentertexttoplogo.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/rectangle.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/spiral.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/topleftmusicalnotes.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wipein.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wipeleft.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wipeout.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wiperight.png +0 -0
- lyrics_transcriber/output/cdgmaker/utils.py +132 -0
- lyrics_transcriber/output/countdown_processor.py +306 -0
- lyrics_transcriber/output/fonts/AvenirNext-Bold.ttf +0 -0
- lyrics_transcriber/output/fonts/DMSans-VariableFont_opsz,wght.ttf +0 -0
- lyrics_transcriber/output/fonts/DMSerifDisplay-Regular.ttf +0 -0
- lyrics_transcriber/output/fonts/Oswald-SemiBold.ttf +0 -0
- lyrics_transcriber/output/fonts/Zurich_Cn_BT_Bold.ttf +0 -0
- lyrics_transcriber/output/fonts/arial.ttf +0 -0
- lyrics_transcriber/output/fonts/georgia.ttf +0 -0
- lyrics_transcriber/output/fonts/verdana.ttf +0 -0
- lyrics_transcriber/output/generator.py +257 -0
- lyrics_transcriber/output/lrc_to_cdg.py +61 -0
- lyrics_transcriber/output/lyrics_file.py +102 -0
- lyrics_transcriber/output/plain_text.py +96 -0
- lyrics_transcriber/output/segment_resizer.py +431 -0
- lyrics_transcriber/output/subtitles.py +397 -0
- lyrics_transcriber/output/video.py +544 -0
- lyrics_transcriber/review/__init__.py +0 -0
- lyrics_transcriber/review/server.py +676 -0
- lyrics_transcriber/storage/__init__.py +0 -0
- lyrics_transcriber/storage/dropbox.py +225 -0
- lyrics_transcriber/transcribers/__init__.py +0 -0
- lyrics_transcriber/transcribers/audioshake.py +379 -0
- lyrics_transcriber/transcribers/base_transcriber.py +157 -0
- lyrics_transcriber/transcribers/whisper.py +330 -0
- lyrics_transcriber/types.py +650 -0
- lyrics_transcriber/utils/__init__.py +0 -0
- lyrics_transcriber/utils/word_utils.py +27 -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
|
+
}
|