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.
- karaoke_gen/audio_fetcher.py +461 -0
- karaoke_gen/audio_processor.py +407 -30
- karaoke_gen/config.py +62 -113
- karaoke_gen/file_handler.py +32 -59
- karaoke_gen/karaoke_finalise/karaoke_finalise.py +148 -67
- karaoke_gen/karaoke_gen.py +270 -61
- karaoke_gen/lyrics_processor.py +13 -1
- karaoke_gen/metadata.py +78 -73
- 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/style_loader.py +531 -0
- karaoke_gen/utils/bulk_cli.py +6 -0
- karaoke_gen/utils/cli_args.py +424 -0
- karaoke_gen/utils/gen_cli.py +26 -261
- karaoke_gen/utils/remote_cli.py +1965 -0
- karaoke_gen/video_background_processor.py +351 -0
- karaoke_gen-0.71.27.dist-info/METADATA +610 -0
- karaoke_gen-0.71.27.dist-info/RECORD +275 -0
- {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.27.dist-info}/WHEEL +1 -1
- {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.27.dist-info}/entry_points.txt +1 -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 +520 -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 +1043 -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 +212 -0
- lyrics_transcriber/frontend/src/api.ts +239 -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 +387 -0
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +1373 -0
- lyrics_transcriber/frontend/src/components/MetricsDashboard.tsx +51 -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 +688 -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-DdJTDWH3.js +42039 -0
- lyrics_transcriber/frontend/web_assets/assets/index-DdJTDWH3.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 +267 -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 +290 -0
- lyrics_transcriber/transcribers/base_transcriber.py +157 -0
- lyrics_transcriber/transcribers/whisper.py +330 -0
- lyrics_transcriber/types.py +648 -0
- lyrics_transcriber/utils/__init__.py +0 -0
- lyrics_transcriber/utils/word_utils.py +27 -0
- karaoke_gen-0.57.0.dist-info/METADATA +0 -167
- karaoke_gen-0.57.0.dist-info/RECORD +0 -23
- {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.27.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,1373 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useMemo, memo } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
AnchorSequence,
|
|
4
|
+
CorrectionData,
|
|
5
|
+
GapSequence,
|
|
6
|
+
HighlightInfo,
|
|
7
|
+
InteractionMode,
|
|
8
|
+
LyricsSegment,
|
|
9
|
+
ReferenceSource,
|
|
10
|
+
WordCorrection,
|
|
11
|
+
CorrectionAnnotation
|
|
12
|
+
} from '../types'
|
|
13
|
+
import { Box, Button, Grid, useMediaQuery, useTheme } from '@mui/material'
|
|
14
|
+
import { ApiClient } from '../api'
|
|
15
|
+
import ReferenceView from './ReferenceView'
|
|
16
|
+
import TranscriptionView from './TranscriptionView'
|
|
17
|
+
import { WordClickInfo, FlashType } from './shared/types'
|
|
18
|
+
import EditModal from './EditModal'
|
|
19
|
+
import ReviewChangesModal from './ReviewChangesModal'
|
|
20
|
+
import ReplaceAllLyricsModal from './ReplaceAllLyricsModal'
|
|
21
|
+
import CorrectionAnnotationModal from './CorrectionAnnotationModal'
|
|
22
|
+
import CorrectionDetailCard from './CorrectionDetailCard'
|
|
23
|
+
import {
|
|
24
|
+
addSegmentBefore,
|
|
25
|
+
splitSegment,
|
|
26
|
+
deleteSegment,
|
|
27
|
+
mergeSegment,
|
|
28
|
+
findAndReplace,
|
|
29
|
+
deleteWord
|
|
30
|
+
} from './shared/utils/segmentOperations'
|
|
31
|
+
import { loadSavedData, saveData, clearSavedData } from './shared/utils/localStorage'
|
|
32
|
+
import { setupKeyboardHandlers, setModalHandler, getModalState } from './shared/utils/keyboardHandlers'
|
|
33
|
+
import Header from './Header'
|
|
34
|
+
import { getWordsFromIds } from './shared/utils/wordUtils'
|
|
35
|
+
import AddLyricsModal from './AddLyricsModal'
|
|
36
|
+
import { RestoreFromTrash, OndemandVideo } from '@mui/icons-material'
|
|
37
|
+
import FindReplaceModal from './FindReplaceModal'
|
|
38
|
+
import TimingOffsetModal from './TimingOffsetModal'
|
|
39
|
+
import { applyOffsetToCorrectionData, applyOffsetToSegment } from './shared/utils/timingUtils'
|
|
40
|
+
|
|
41
|
+
// Add type for window augmentation at the top of the file
|
|
42
|
+
declare global {
|
|
43
|
+
interface Window {
|
|
44
|
+
toggleAudioPlayback?: () => void;
|
|
45
|
+
seekAndPlayAudio?: (startTime: number) => void;
|
|
46
|
+
getAudioDuration?: () => number;
|
|
47
|
+
isAudioPlaying?: boolean;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const debugLog = false;
|
|
52
|
+
export interface LyricsAnalyzerProps {
|
|
53
|
+
data: CorrectionData
|
|
54
|
+
onFileLoad: () => void
|
|
55
|
+
onShowMetadata: () => void
|
|
56
|
+
apiClient: ApiClient | null
|
|
57
|
+
isReadOnly: boolean
|
|
58
|
+
audioHash: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type ModalContent = {
|
|
62
|
+
type: 'anchor'
|
|
63
|
+
data: AnchorSequence & {
|
|
64
|
+
wordId: string
|
|
65
|
+
word?: string
|
|
66
|
+
anchor_sequences: AnchorSequence[]
|
|
67
|
+
}
|
|
68
|
+
} | {
|
|
69
|
+
type: 'gap'
|
|
70
|
+
data: GapSequence & {
|
|
71
|
+
wordId: string
|
|
72
|
+
word: string
|
|
73
|
+
anchor_sequences: AnchorSequence[]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Define types for the memoized components
|
|
78
|
+
interface MemoizedTranscriptionViewProps {
|
|
79
|
+
data: CorrectionData
|
|
80
|
+
mode: InteractionMode
|
|
81
|
+
onElementClick: (content: ModalContent) => void
|
|
82
|
+
onWordClick: (info: WordClickInfo) => void
|
|
83
|
+
flashingType: FlashType
|
|
84
|
+
flashingHandler: string | null
|
|
85
|
+
highlightInfo: HighlightInfo | null
|
|
86
|
+
onPlaySegment?: (time: number) => void
|
|
87
|
+
currentTime: number
|
|
88
|
+
anchors: AnchorSequence[]
|
|
89
|
+
disableHighlighting: boolean
|
|
90
|
+
onDataChange?: (updatedData: CorrectionData) => void
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Create a memoized TranscriptionView component
|
|
94
|
+
const MemoizedTranscriptionView = memo(function MemoizedTranscriptionView({
|
|
95
|
+
data,
|
|
96
|
+
mode,
|
|
97
|
+
onElementClick,
|
|
98
|
+
onWordClick,
|
|
99
|
+
flashingType,
|
|
100
|
+
flashingHandler,
|
|
101
|
+
highlightInfo,
|
|
102
|
+
onPlaySegment,
|
|
103
|
+
currentTime,
|
|
104
|
+
anchors,
|
|
105
|
+
disableHighlighting,
|
|
106
|
+
onDataChange
|
|
107
|
+
}: MemoizedTranscriptionViewProps) {
|
|
108
|
+
return (
|
|
109
|
+
<TranscriptionView
|
|
110
|
+
data={data}
|
|
111
|
+
mode={mode}
|
|
112
|
+
onElementClick={onElementClick}
|
|
113
|
+
onWordClick={onWordClick}
|
|
114
|
+
flashingType={flashingType}
|
|
115
|
+
flashingHandler={flashingHandler}
|
|
116
|
+
highlightInfo={highlightInfo}
|
|
117
|
+
onPlaySegment={onPlaySegment}
|
|
118
|
+
currentTime={disableHighlighting ? undefined : currentTime}
|
|
119
|
+
anchors={anchors}
|
|
120
|
+
onDataChange={onDataChange}
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
interface MemoizedReferenceViewProps {
|
|
126
|
+
referenceSources: Record<string, ReferenceSource>
|
|
127
|
+
anchors: AnchorSequence[]
|
|
128
|
+
gaps: GapSequence[]
|
|
129
|
+
mode: InteractionMode
|
|
130
|
+
onElementClick: (content: ModalContent) => void
|
|
131
|
+
onWordClick: (info: WordClickInfo) => void
|
|
132
|
+
flashingType: FlashType
|
|
133
|
+
highlightInfo: HighlightInfo | null
|
|
134
|
+
currentSource: string
|
|
135
|
+
onSourceChange: (source: string) => void
|
|
136
|
+
corrected_segments: LyricsSegment[]
|
|
137
|
+
corrections: WordCorrection[]
|
|
138
|
+
onAddLyrics?: () => void
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Create a memoized ReferenceView component
|
|
142
|
+
const MemoizedReferenceView = memo(function MemoizedReferenceView({
|
|
143
|
+
referenceSources,
|
|
144
|
+
anchors,
|
|
145
|
+
gaps,
|
|
146
|
+
mode,
|
|
147
|
+
onElementClick,
|
|
148
|
+
onWordClick,
|
|
149
|
+
flashingType,
|
|
150
|
+
highlightInfo,
|
|
151
|
+
currentSource,
|
|
152
|
+
onSourceChange,
|
|
153
|
+
corrected_segments,
|
|
154
|
+
corrections,
|
|
155
|
+
onAddLyrics
|
|
156
|
+
}: MemoizedReferenceViewProps) {
|
|
157
|
+
return (
|
|
158
|
+
<ReferenceView
|
|
159
|
+
referenceSources={referenceSources}
|
|
160
|
+
anchors={anchors}
|
|
161
|
+
gaps={gaps}
|
|
162
|
+
mode={mode}
|
|
163
|
+
onElementClick={onElementClick}
|
|
164
|
+
onWordClick={onWordClick}
|
|
165
|
+
flashingType={flashingType}
|
|
166
|
+
highlightInfo={highlightInfo}
|
|
167
|
+
currentSource={currentSource}
|
|
168
|
+
onSourceChange={onSourceChange}
|
|
169
|
+
corrected_segments={corrected_segments}
|
|
170
|
+
corrections={corrections}
|
|
171
|
+
onAddLyrics={onAddLyrics}
|
|
172
|
+
/>
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
interface MemoizedHeaderProps {
|
|
177
|
+
isReadOnly: boolean
|
|
178
|
+
onFileLoad: () => void
|
|
179
|
+
data: CorrectionData
|
|
180
|
+
onMetricClick: {
|
|
181
|
+
anchor: () => void
|
|
182
|
+
corrected: () => void
|
|
183
|
+
uncorrected: () => void
|
|
184
|
+
}
|
|
185
|
+
effectiveMode: InteractionMode
|
|
186
|
+
onModeChange: (mode: InteractionMode) => void
|
|
187
|
+
apiClient: ApiClient | null
|
|
188
|
+
audioHash: string
|
|
189
|
+
onTimeUpdate: (time: number) => void
|
|
190
|
+
onHandlerToggle: (handler: string, enabled: boolean) => void
|
|
191
|
+
isUpdatingHandlers: boolean
|
|
192
|
+
onHandlerClick?: (handler: string) => void
|
|
193
|
+
onAddLyrics?: () => void
|
|
194
|
+
onFindReplace?: () => void
|
|
195
|
+
onEditAll?: () => void
|
|
196
|
+
onTimingOffset: () => void
|
|
197
|
+
timingOffsetMs: number
|
|
198
|
+
onUndo: () => void
|
|
199
|
+
onRedo: () => void
|
|
200
|
+
canUndo: boolean
|
|
201
|
+
canRedo: boolean
|
|
202
|
+
onUnCorrectAll: () => void
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Create a memoized Header component
|
|
206
|
+
const MemoizedHeader = memo(function MemoizedHeader({
|
|
207
|
+
isReadOnly,
|
|
208
|
+
onFileLoad,
|
|
209
|
+
data,
|
|
210
|
+
onMetricClick,
|
|
211
|
+
effectiveMode,
|
|
212
|
+
onModeChange,
|
|
213
|
+
apiClient,
|
|
214
|
+
audioHash,
|
|
215
|
+
onTimeUpdate,
|
|
216
|
+
onHandlerToggle,
|
|
217
|
+
isUpdatingHandlers,
|
|
218
|
+
onHandlerClick,
|
|
219
|
+
onFindReplace,
|
|
220
|
+
onEditAll,
|
|
221
|
+
onTimingOffset,
|
|
222
|
+
timingOffsetMs,
|
|
223
|
+
onUndo,
|
|
224
|
+
onRedo,
|
|
225
|
+
canUndo,
|
|
226
|
+
canRedo,
|
|
227
|
+
onUnCorrectAll
|
|
228
|
+
}: MemoizedHeaderProps) {
|
|
229
|
+
return (
|
|
230
|
+
<Header
|
|
231
|
+
isReadOnly={isReadOnly}
|
|
232
|
+
onFileLoad={onFileLoad}
|
|
233
|
+
data={data}
|
|
234
|
+
onMetricClick={onMetricClick}
|
|
235
|
+
effectiveMode={effectiveMode}
|
|
236
|
+
onModeChange={onModeChange}
|
|
237
|
+
apiClient={apiClient}
|
|
238
|
+
audioHash={audioHash}
|
|
239
|
+
onTimeUpdate={onTimeUpdate}
|
|
240
|
+
onHandlerToggle={onHandlerToggle}
|
|
241
|
+
isUpdatingHandlers={isUpdatingHandlers}
|
|
242
|
+
onHandlerClick={onHandlerClick}
|
|
243
|
+
onFindReplace={onFindReplace}
|
|
244
|
+
onEditAll={onEditAll}
|
|
245
|
+
onTimingOffset={onTimingOffset}
|
|
246
|
+
timingOffsetMs={timingOffsetMs}
|
|
247
|
+
onUndo={onUndo}
|
|
248
|
+
onRedo={onRedo}
|
|
249
|
+
canUndo={canUndo}
|
|
250
|
+
canRedo={canRedo}
|
|
251
|
+
onUnCorrectAll={onUnCorrectAll}
|
|
252
|
+
/>
|
|
253
|
+
);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly, audioHash }: LyricsAnalyzerProps) {
|
|
257
|
+
const [modalContent, setModalContent] = useState<ModalContent | null>(null)
|
|
258
|
+
const [flashingType, setFlashingType] = useState<FlashType>(null)
|
|
259
|
+
const [highlightInfo, setHighlightInfo] = useState<HighlightInfo | null>(null)
|
|
260
|
+
const [currentSource, setCurrentSource] = useState<string>(() => {
|
|
261
|
+
if (!initialData?.reference_lyrics) {
|
|
262
|
+
return ''
|
|
263
|
+
}
|
|
264
|
+
const availableSources = Object.keys(initialData.reference_lyrics)
|
|
265
|
+
return availableSources.length > 0 ? availableSources[0] : ''
|
|
266
|
+
})
|
|
267
|
+
const [isReviewComplete, setIsReviewComplete] = useState(false)
|
|
268
|
+
const [originalData] = useState(() => JSON.parse(JSON.stringify(initialData)))
|
|
269
|
+
const [interactionMode, setInteractionMode] = useState<InteractionMode>('edit')
|
|
270
|
+
const [isShiftPressed, setIsShiftPressed] = useState(false)
|
|
271
|
+
const [isCtrlPressed, setIsCtrlPressed] = useState(false)
|
|
272
|
+
const [editModalSegment, setEditModalSegment] = useState<{
|
|
273
|
+
segment: LyricsSegment
|
|
274
|
+
index: number
|
|
275
|
+
originalSegment: LyricsSegment
|
|
276
|
+
} | null>(null)
|
|
277
|
+
const [isReplaceAllLyricsModalOpen, setIsReplaceAllLyricsModalOpen] = useState(false)
|
|
278
|
+
const [isReviewModalOpen, setIsReviewModalOpen] = useState(false)
|
|
279
|
+
const [currentAudioTime, setCurrentAudioTime] = useState(0)
|
|
280
|
+
const [isUpdatingHandlers, setIsUpdatingHandlers] = useState(false)
|
|
281
|
+
const [flashingHandler, setFlashingHandler] = useState<string | null>(null)
|
|
282
|
+
const [isAddingLyrics, setIsAddingLyrics] = useState(false)
|
|
283
|
+
const [isAddLyricsModalOpen, setIsAddLyricsModalOpen] = useState(false)
|
|
284
|
+
const [isAnyModalOpen, setIsAnyModalOpen] = useState(false)
|
|
285
|
+
const [isFindReplaceModalOpen, setIsFindReplaceModalOpen] = useState(false)
|
|
286
|
+
const [isTimingOffsetModalOpen, setIsTimingOffsetModalOpen] = useState(false)
|
|
287
|
+
const [timingOffsetMs, setTimingOffsetMs] = useState(0)
|
|
288
|
+
|
|
289
|
+
// Annotation collection state
|
|
290
|
+
const [annotations, setAnnotations] = useState<Omit<CorrectionAnnotation, 'annotation_id' | 'timestamp'>[]>([])
|
|
291
|
+
const [isAnnotationModalOpen, setIsAnnotationModalOpen] = useState(false)
|
|
292
|
+
const [pendingAnnotation, setPendingAnnotation] = useState<{
|
|
293
|
+
originalText: string
|
|
294
|
+
correctedText: string
|
|
295
|
+
wordIdsAffected: string[]
|
|
296
|
+
gapId?: string
|
|
297
|
+
} | null>(null)
|
|
298
|
+
const [annotationsEnabled] = useState(() => {
|
|
299
|
+
// Check localStorage for user preference
|
|
300
|
+
const saved = localStorage.getItem('annotationsEnabled')
|
|
301
|
+
return saved !== null ? saved === 'true' : true // Default: enabled
|
|
302
|
+
// TODO: Add UI toggle to enable/disable via setAnnotationsEnabled
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
// Correction detail card state
|
|
306
|
+
const [correctionDetailOpen, setCorrectionDetailOpen] = useState(false)
|
|
307
|
+
const [selectedCorrection, setSelectedCorrection] = useState<{
|
|
308
|
+
wordId: string
|
|
309
|
+
originalWord: string
|
|
310
|
+
correctedWord: string
|
|
311
|
+
category: string | null
|
|
312
|
+
confidence: number
|
|
313
|
+
reason: string
|
|
314
|
+
handler: string
|
|
315
|
+
source: string
|
|
316
|
+
} | null>(null)
|
|
317
|
+
|
|
318
|
+
const theme = useTheme()
|
|
319
|
+
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
|
|
320
|
+
|
|
321
|
+
// State history for Undo/Redo
|
|
322
|
+
const [history, setHistory] = useState<CorrectionData[]>([initialData])
|
|
323
|
+
const [historyIndex, setHistoryIndex] = useState(0)
|
|
324
|
+
|
|
325
|
+
// Derived state: the current data based on history index
|
|
326
|
+
const data = history[historyIndex];
|
|
327
|
+
|
|
328
|
+
// Function to update data and manage history
|
|
329
|
+
const updateDataWithHistory = useCallback((newData: CorrectionData, actionDescription?: string) => {
|
|
330
|
+
if (debugLog) {
|
|
331
|
+
console.log(`[DEBUG] updateDataWithHistory: Action - ${actionDescription || 'Unknown'}. Current index: ${historyIndex}, History length: ${history.length}`);
|
|
332
|
+
}
|
|
333
|
+
const newHistory = history.slice(0, historyIndex + 1)
|
|
334
|
+
const deepCopiedNewData = JSON.parse(JSON.stringify(newData));
|
|
335
|
+
|
|
336
|
+
newHistory.push(deepCopiedNewData)
|
|
337
|
+
setHistory(newHistory)
|
|
338
|
+
setHistoryIndex(newHistory.length - 1)
|
|
339
|
+
if (debugLog) {
|
|
340
|
+
console.log(`[DEBUG] updateDataWithHistory: History updated. New index: ${newHistory.length - 1}, New length: ${newHistory.length}`);
|
|
341
|
+
}
|
|
342
|
+
}, [history, historyIndex])
|
|
343
|
+
|
|
344
|
+
// Reset history when initial data changes (e.g., new file loaded)
|
|
345
|
+
useEffect(() => {
|
|
346
|
+
setHistory([initialData])
|
|
347
|
+
setHistoryIndex(0)
|
|
348
|
+
}, [initialData])
|
|
349
|
+
|
|
350
|
+
// Update debug logging to use new ID-based structure
|
|
351
|
+
useEffect(() => {
|
|
352
|
+
if (debugLog) {
|
|
353
|
+
console.log('LyricsAnalyzer Initial Data:', {
|
|
354
|
+
hasData: !!initialData,
|
|
355
|
+
segmentsCount: initialData?.corrected_segments?.length ?? 0,
|
|
356
|
+
anchorsCount: initialData?.anchor_sequences?.length ?? 0,
|
|
357
|
+
gapsCount: initialData?.gap_sequences?.length ?? 0,
|
|
358
|
+
firstAnchor: initialData?.anchor_sequences?.[0] && {
|
|
359
|
+
transcribedWordIds: initialData.anchor_sequences[0].transcribed_word_ids,
|
|
360
|
+
referenceWordIds: initialData.anchor_sequences[0].reference_word_ids
|
|
361
|
+
},
|
|
362
|
+
firstSegment: initialData?.corrected_segments?.[0],
|
|
363
|
+
referenceSources: Object.keys(initialData?.reference_lyrics ?? {})
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}, [initialData]);
|
|
367
|
+
|
|
368
|
+
// Load saved data
|
|
369
|
+
useEffect(() => {
|
|
370
|
+
const savedData = loadSavedData(initialData)
|
|
371
|
+
if (savedData && window.confirm('Found saved progress for this song. Would you like to restore it?')) {
|
|
372
|
+
// Replace history with saved data as the initial state
|
|
373
|
+
setHistory([savedData])
|
|
374
|
+
setHistoryIndex(0)
|
|
375
|
+
}
|
|
376
|
+
}, [initialData]) // Keep dependency only on initialData
|
|
377
|
+
|
|
378
|
+
// Save data - This should save the *current* state, not affect history
|
|
379
|
+
useEffect(() => {
|
|
380
|
+
if (!isReadOnly) {
|
|
381
|
+
saveData(data, initialData) // Use 'data' derived from history and the initialData prop
|
|
382
|
+
}
|
|
383
|
+
}, [data, isReadOnly, initialData]) // Correct dependencies
|
|
384
|
+
|
|
385
|
+
// Keyboard handlers
|
|
386
|
+
useEffect(() => {
|
|
387
|
+
const { currentModalHandler } = getModalState()
|
|
388
|
+
|
|
389
|
+
if (debugLog) {
|
|
390
|
+
console.log('LyricsAnalyzer - Setting up keyboard effect', {
|
|
391
|
+
isAnyModalOpen,
|
|
392
|
+
hasSpacebarHandler: !!currentModalHandler
|
|
393
|
+
})
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const { handleKeyDown, handleKeyUp, cleanup } = setupKeyboardHandlers({
|
|
397
|
+
setIsShiftPressed,
|
|
398
|
+
setIsCtrlPressed
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
// Always add keyboard listeners
|
|
402
|
+
if (debugLog) {
|
|
403
|
+
console.log('LyricsAnalyzer - Adding keyboard event listeners')
|
|
404
|
+
}
|
|
405
|
+
window.addEventListener('keydown', handleKeyDown)
|
|
406
|
+
window.addEventListener('keyup', handleKeyUp)
|
|
407
|
+
|
|
408
|
+
// Reset modifier states when a modal opens
|
|
409
|
+
if (isAnyModalOpen) {
|
|
410
|
+
setIsShiftPressed(false)
|
|
411
|
+
setIsCtrlPressed(false)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Cleanup function
|
|
415
|
+
return () => {
|
|
416
|
+
if (debugLog) {
|
|
417
|
+
console.log('LyricsAnalyzer - Cleanup effect running')
|
|
418
|
+
}
|
|
419
|
+
window.removeEventListener('keydown', handleKeyDown)
|
|
420
|
+
window.removeEventListener('keyup', handleKeyUp)
|
|
421
|
+
document.body.style.userSelect = ''
|
|
422
|
+
// Call the cleanup function to remove window blur/focus listeners
|
|
423
|
+
cleanup()
|
|
424
|
+
}
|
|
425
|
+
}, [setIsShiftPressed, setIsCtrlPressed, isAnyModalOpen])
|
|
426
|
+
|
|
427
|
+
// Update modal state tracking
|
|
428
|
+
useEffect(() => {
|
|
429
|
+
const modalOpen = Boolean(
|
|
430
|
+
modalContent ||
|
|
431
|
+
editModalSegment ||
|
|
432
|
+
isReviewModalOpen ||
|
|
433
|
+
isAddLyricsModalOpen ||
|
|
434
|
+
isFindReplaceModalOpen ||
|
|
435
|
+
isReplaceAllLyricsModalOpen ||
|
|
436
|
+
isTimingOffsetModalOpen
|
|
437
|
+
)
|
|
438
|
+
setIsAnyModalOpen(modalOpen)
|
|
439
|
+
}, [modalContent, editModalSegment, isReviewModalOpen, isAddLyricsModalOpen, isFindReplaceModalOpen, isReplaceAllLyricsModalOpen, isTimingOffsetModalOpen])
|
|
440
|
+
|
|
441
|
+
// Calculate effective mode based on modifier key states
|
|
442
|
+
const effectiveMode = isCtrlPressed ? 'delete_word' : (isShiftPressed ? 'highlight' : interactionMode)
|
|
443
|
+
|
|
444
|
+
const handleFlash = useCallback((type: FlashType, info?: HighlightInfo) => {
|
|
445
|
+
setFlashingType(null)
|
|
446
|
+
setHighlightInfo(null)
|
|
447
|
+
|
|
448
|
+
requestAnimationFrame(() => {
|
|
449
|
+
requestAnimationFrame(() => {
|
|
450
|
+
setFlashingType(type)
|
|
451
|
+
if (info) {
|
|
452
|
+
setHighlightInfo(info)
|
|
453
|
+
}
|
|
454
|
+
setTimeout(() => {
|
|
455
|
+
setFlashingType(null)
|
|
456
|
+
setHighlightInfo(null)
|
|
457
|
+
}, 1200)
|
|
458
|
+
})
|
|
459
|
+
})
|
|
460
|
+
}, [])
|
|
461
|
+
|
|
462
|
+
const handleWordClick = useCallback((info: WordClickInfo) => {
|
|
463
|
+
if (debugLog) {
|
|
464
|
+
console.log('LyricsAnalyzer handleWordClick:', { info });
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (effectiveMode === 'delete_word') {
|
|
468
|
+
// Use the shared deleteWord utility function
|
|
469
|
+
const newData = deleteWord(data, info.word_id);
|
|
470
|
+
updateDataWithHistory(newData, 'delete word'); // Update history
|
|
471
|
+
|
|
472
|
+
// Flash to indicate the word was deleted
|
|
473
|
+
handleFlash('word');
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (effectiveMode === 'highlight') {
|
|
478
|
+
// Find if this word is part of a correction
|
|
479
|
+
const correction = data.corrections?.find(c =>
|
|
480
|
+
c.corrected_word_id === info.word_id ||
|
|
481
|
+
c.word_id === info.word_id
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
if (correction) {
|
|
485
|
+
// For agentic corrections, show the detail card instead of just highlighting
|
|
486
|
+
if (correction.handler === 'AgenticCorrector') {
|
|
487
|
+
handleShowCorrectionDetail(info.word_id)
|
|
488
|
+
return
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
setHighlightInfo({
|
|
492
|
+
type: 'correction',
|
|
493
|
+
transcribed_words: [], // Required by type but not used for corrections
|
|
494
|
+
correction: correction
|
|
495
|
+
});
|
|
496
|
+
setFlashingType('word');
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Find if this word is part of an anchor sequence
|
|
501
|
+
const anchor = data.anchor_sequences?.find(a =>
|
|
502
|
+
a.transcribed_word_ids.includes(info.word_id) ||
|
|
503
|
+
Object.values(a.reference_word_ids).some(ids =>
|
|
504
|
+
ids.includes(info.word_id)
|
|
505
|
+
)
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
if (anchor) {
|
|
509
|
+
// Create a temporary segment containing all words
|
|
510
|
+
const allWords = data.corrected_segments.flatMap(s => s.words)
|
|
511
|
+
const tempSegment: LyricsSegment = {
|
|
512
|
+
id: 'temp',
|
|
513
|
+
words: allWords,
|
|
514
|
+
text: allWords.map(w => w.text).join(' '),
|
|
515
|
+
start_time: allWords[0]?.start_time ?? null,
|
|
516
|
+
end_time: allWords[allWords.length - 1]?.end_time ?? null
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const transcribedWords = getWordsFromIds(
|
|
520
|
+
[tempSegment],
|
|
521
|
+
anchor.transcribed_word_ids
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
const referenceWords = Object.fromEntries(
|
|
525
|
+
Object.entries(anchor.reference_word_ids).map(([source, ids]) => {
|
|
526
|
+
const sourceWords = data.reference_lyrics[source].segments.flatMap(s => s.words)
|
|
527
|
+
const tempSourceSegment: LyricsSegment = {
|
|
528
|
+
id: `temp-${source}`,
|
|
529
|
+
words: sourceWords,
|
|
530
|
+
text: sourceWords.map(w => w.text).join(' '),
|
|
531
|
+
start_time: sourceWords[0]?.start_time ?? null,
|
|
532
|
+
end_time: sourceWords[sourceWords.length - 1]?.end_time ?? null
|
|
533
|
+
}
|
|
534
|
+
return [source, getWordsFromIds([tempSourceSegment], ids)]
|
|
535
|
+
})
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
setHighlightInfo({
|
|
539
|
+
type: 'anchor',
|
|
540
|
+
sequence: anchor,
|
|
541
|
+
transcribed_words: transcribedWords,
|
|
542
|
+
reference_words: referenceWords
|
|
543
|
+
});
|
|
544
|
+
setFlashingType('word');
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Find if this word is part of a gap sequence
|
|
549
|
+
const gap = data.gap_sequences?.find(g =>
|
|
550
|
+
g.transcribed_word_ids.includes(info.word_id)
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
if (gap) {
|
|
554
|
+
// Create a temporary segment containing all words
|
|
555
|
+
const allWords = data.corrected_segments.flatMap(s => s.words)
|
|
556
|
+
const tempSegment: LyricsSegment = {
|
|
557
|
+
id: 'temp',
|
|
558
|
+
words: allWords,
|
|
559
|
+
text: allWords.map(w => w.text).join(' '),
|
|
560
|
+
start_time: allWords[0]?.start_time ?? null,
|
|
561
|
+
end_time: allWords[allWords.length - 1]?.end_time ?? null
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const transcribedWords = getWordsFromIds(
|
|
565
|
+
[tempSegment],
|
|
566
|
+
gap.transcribed_word_ids
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
const referenceWords = Object.fromEntries(
|
|
570
|
+
Object.entries(gap.reference_word_ids).map(([source, ids]) => {
|
|
571
|
+
const sourceWords = data.reference_lyrics[source].segments.flatMap(s => s.words)
|
|
572
|
+
const tempSourceSegment: LyricsSegment = {
|
|
573
|
+
id: `temp-${source}`,
|
|
574
|
+
words: sourceWords,
|
|
575
|
+
text: sourceWords.map(w => w.text).join(' '),
|
|
576
|
+
start_time: sourceWords[0]?.start_time ?? null,
|
|
577
|
+
end_time: sourceWords[sourceWords.length - 1]?.end_time ?? null
|
|
578
|
+
}
|
|
579
|
+
return [source, getWordsFromIds([tempSourceSegment], ids)]
|
|
580
|
+
})
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
setHighlightInfo({
|
|
584
|
+
type: 'gap',
|
|
585
|
+
sequence: gap,
|
|
586
|
+
transcribed_words: transcribedWords,
|
|
587
|
+
reference_words: referenceWords
|
|
588
|
+
});
|
|
589
|
+
setFlashingType('word');
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
} else if (effectiveMode === 'edit') {
|
|
593
|
+
// Find the segment containing this word
|
|
594
|
+
const segmentIndex = data.corrected_segments.findIndex(segment =>
|
|
595
|
+
segment.words.some(word => word.id === info.word_id)
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
if (segmentIndex !== -1) {
|
|
599
|
+
const segment = data.corrected_segments[segmentIndex];
|
|
600
|
+
setEditModalSegment({
|
|
601
|
+
segment,
|
|
602
|
+
index: segmentIndex,
|
|
603
|
+
originalSegment: JSON.parse(JSON.stringify(segment))
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}, [data, effectiveMode, setModalContent, handleFlash, deleteWord, updateDataWithHistory]);
|
|
608
|
+
|
|
609
|
+
// Annotation handlers (declared early for use in other callbacks)
|
|
610
|
+
const handleSaveAnnotation = useCallback((annotation: Omit<CorrectionAnnotation, 'annotation_id' | 'timestamp'>) => {
|
|
611
|
+
setAnnotations(prev => [...prev, annotation])
|
|
612
|
+
console.log('Annotation saved:', annotation)
|
|
613
|
+
}, [])
|
|
614
|
+
|
|
615
|
+
const handleSkipAnnotation = useCallback(() => {
|
|
616
|
+
console.log('Annotation skipped')
|
|
617
|
+
}, [])
|
|
618
|
+
|
|
619
|
+
const triggerAnnotationModal = useCallback((
|
|
620
|
+
originalText: string,
|
|
621
|
+
correctedText: string,
|
|
622
|
+
wordIdsAffected: string[],
|
|
623
|
+
gapId?: string
|
|
624
|
+
) => {
|
|
625
|
+
if (!annotationsEnabled || isReadOnly) {
|
|
626
|
+
return
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
setPendingAnnotation({
|
|
630
|
+
originalText,
|
|
631
|
+
correctedText,
|
|
632
|
+
wordIdsAffected,
|
|
633
|
+
gapId
|
|
634
|
+
})
|
|
635
|
+
setIsAnnotationModalOpen(true)
|
|
636
|
+
}, [annotationsEnabled, isReadOnly])
|
|
637
|
+
|
|
638
|
+
const handleUpdateSegment = useCallback((updatedSegment: LyricsSegment) => {
|
|
639
|
+
if (!editModalSegment) return
|
|
640
|
+
|
|
641
|
+
if (debugLog) {
|
|
642
|
+
console.log('[DEBUG] handleUpdateSegment: Updating history from modal save', {
|
|
643
|
+
segmentIndex: editModalSegment.index,
|
|
644
|
+
currentHistoryIndex: historyIndex,
|
|
645
|
+
currentHistoryLength: history.length,
|
|
646
|
+
currentSegmentText: history[historyIndex]?.corrected_segments[editModalSegment.index]?.text,
|
|
647
|
+
updatedSegmentText: updatedSegment.text
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// --- Ensure Immutability Here ---
|
|
652
|
+
const currentData = history[historyIndex];
|
|
653
|
+
const newSegments = currentData.corrected_segments.map((segment, i) =>
|
|
654
|
+
i === editModalSegment.index ? updatedSegment : segment
|
|
655
|
+
);
|
|
656
|
+
const newDataImmutable: CorrectionData = {
|
|
657
|
+
...currentData,
|
|
658
|
+
corrected_segments: newSegments,
|
|
659
|
+
};
|
|
660
|
+
// --- End Immutability Ensure ---
|
|
661
|
+
|
|
662
|
+
updateDataWithHistory(newDataImmutable, 'update segment');
|
|
663
|
+
|
|
664
|
+
if (debugLog) {
|
|
665
|
+
console.log('[DEBUG] handleUpdateSegment: History updated (async)', {
|
|
666
|
+
newHistoryIndex: historyIndex + 1,
|
|
667
|
+
newHistoryLength: history.length - historyIndex === 1 ? history.length + 1 : historyIndex + 2
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Trigger annotation modal if enabled and text changed
|
|
672
|
+
const originalSegment = editModalSegment.originalSegment || editModalSegment.segment
|
|
673
|
+
if (originalSegment && originalSegment.text !== updatedSegment.text) {
|
|
674
|
+
// Get word IDs that were affected
|
|
675
|
+
const wordIds = updatedSegment.words.map(w => w.id)
|
|
676
|
+
triggerAnnotationModal(
|
|
677
|
+
originalSegment.text,
|
|
678
|
+
updatedSegment.text,
|
|
679
|
+
wordIds
|
|
680
|
+
)
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
setEditModalSegment(null)
|
|
684
|
+
}, [history, historyIndex, editModalSegment, updateDataWithHistory, triggerAnnotationModal])
|
|
685
|
+
|
|
686
|
+
const handleDeleteSegment = useCallback((segmentIndex: number) => {
|
|
687
|
+
const newData = deleteSegment(data, segmentIndex)
|
|
688
|
+
updateDataWithHistory(newData, 'delete segment')
|
|
689
|
+
}, [data, updateDataWithHistory])
|
|
690
|
+
|
|
691
|
+
// Correction action handlers
|
|
692
|
+
const handleRevertCorrection = useCallback((wordId: string) => {
|
|
693
|
+
// Find the correction for this word
|
|
694
|
+
const correction = data.corrections?.find(c =>
|
|
695
|
+
c.corrected_word_id === wordId || c.word_id === wordId
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
if (!correction) {
|
|
699
|
+
console.error('Correction not found for word:', wordId)
|
|
700
|
+
return
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Find the segment containing the corrected word
|
|
704
|
+
const segmentIndex = data.corrected_segments.findIndex(segment =>
|
|
705
|
+
segment.words.some(w => w.id === wordId)
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
if (segmentIndex === -1) {
|
|
709
|
+
console.error('Segment not found for word:', wordId)
|
|
710
|
+
return
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const segment = data.corrected_segments[segmentIndex]
|
|
714
|
+
|
|
715
|
+
// Replace the corrected word with the original
|
|
716
|
+
const newWords = segment.words.map(word => {
|
|
717
|
+
if (word.id === wordId) {
|
|
718
|
+
return {
|
|
719
|
+
...word,
|
|
720
|
+
text: correction.original_word,
|
|
721
|
+
id: correction.word_id // Restore original word ID
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return word
|
|
725
|
+
})
|
|
726
|
+
|
|
727
|
+
// Rebuild segment text
|
|
728
|
+
const newText = newWords.map(w => w.text).join(' ')
|
|
729
|
+
|
|
730
|
+
const newSegment = {
|
|
731
|
+
...segment,
|
|
732
|
+
words: newWords,
|
|
733
|
+
text: newText
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Update data
|
|
737
|
+
const newSegments = data.corrected_segments.map((seg, idx) =>
|
|
738
|
+
idx === segmentIndex ? newSegment : seg
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
// Remove the correction from the corrections list
|
|
742
|
+
const newCorrections = data.corrections?.filter(c =>
|
|
743
|
+
c.corrected_word_id !== wordId && c.word_id !== wordId
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
const newData: CorrectionData = {
|
|
747
|
+
...data,
|
|
748
|
+
corrected_segments: newSegments,
|
|
749
|
+
corrections: newCorrections || []
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
updateDataWithHistory(newData, 'revert correction')
|
|
753
|
+
|
|
754
|
+
console.log('Reverted correction:', {
|
|
755
|
+
originalWord: correction.original_word,
|
|
756
|
+
correctedWord: correction.corrected_word,
|
|
757
|
+
wordId
|
|
758
|
+
})
|
|
759
|
+
}, [data, updateDataWithHistory])
|
|
760
|
+
|
|
761
|
+
const handleEditCorrection = useCallback((wordId: string) => {
|
|
762
|
+
// Find the segment containing this word
|
|
763
|
+
const segmentIndex = data.corrected_segments.findIndex(segment =>
|
|
764
|
+
segment.words.some(w => w.id === wordId)
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
if (segmentIndex === -1) {
|
|
768
|
+
console.error('Segment not found for word:', wordId)
|
|
769
|
+
return
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Open edit modal for this segment
|
|
773
|
+
const segment = data.corrected_segments[segmentIndex]
|
|
774
|
+
setEditModalSegment({
|
|
775
|
+
segment: segment,
|
|
776
|
+
index: segmentIndex,
|
|
777
|
+
originalSegment: segment
|
|
778
|
+
})
|
|
779
|
+
}, [data])
|
|
780
|
+
|
|
781
|
+
const handleAcceptCorrection = useCallback((wordId: string) => {
|
|
782
|
+
// For now, just log acceptance
|
|
783
|
+
// In the future, this could be tracked in the annotation system
|
|
784
|
+
console.log('Accepted correction for word:', wordId)
|
|
785
|
+
|
|
786
|
+
// TODO: Track acceptance in annotation system
|
|
787
|
+
// This could be used to build confidence in the AI's corrections over time
|
|
788
|
+
}, [])
|
|
789
|
+
|
|
790
|
+
const handleShowCorrectionDetail = useCallback((wordId: string) => {
|
|
791
|
+
// Find the correction for this word
|
|
792
|
+
const correction = data.corrections?.find(c =>
|
|
793
|
+
c.corrected_word_id === wordId || c.word_id === wordId
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
if (!correction) {
|
|
797
|
+
console.error('Correction not found for word:', wordId)
|
|
798
|
+
return
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Extract category from reason (format: "reason [CATEGORY] (confidence: XX%)")
|
|
802
|
+
const categoryMatch = correction.reason?.match(/\[([A-Z_]+)\]/)
|
|
803
|
+
const category = categoryMatch ? categoryMatch[1] : null
|
|
804
|
+
|
|
805
|
+
// Find the corrected word text
|
|
806
|
+
const correctedWord = data.corrected_segments
|
|
807
|
+
.flatMap(s => s.words)
|
|
808
|
+
.find(w => w.id === wordId)?.text || correction.corrected_word
|
|
809
|
+
|
|
810
|
+
setSelectedCorrection({
|
|
811
|
+
wordId,
|
|
812
|
+
originalWord: correction.original_word,
|
|
813
|
+
correctedWord: correctedWord,
|
|
814
|
+
category,
|
|
815
|
+
confidence: correction.confidence,
|
|
816
|
+
reason: correction.reason,
|
|
817
|
+
handler: correction.handler,
|
|
818
|
+
source: correction.source
|
|
819
|
+
})
|
|
820
|
+
setCorrectionDetailOpen(true)
|
|
821
|
+
}, [data])
|
|
822
|
+
|
|
823
|
+
const handleFinishReview = useCallback(() => {
|
|
824
|
+
console.log(`[TIMING] handleFinishReview - Current timing offset: ${timingOffsetMs}ms`);
|
|
825
|
+
setIsReviewModalOpen(true)
|
|
826
|
+
}, [timingOffsetMs])
|
|
827
|
+
|
|
828
|
+
const handleSubmitToServer = useCallback(async () => {
|
|
829
|
+
if (!apiClient) return
|
|
830
|
+
|
|
831
|
+
try {
|
|
832
|
+
if (debugLog) {
|
|
833
|
+
console.log('Submitting changes to server')
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Debug logging for timing offset
|
|
837
|
+
console.log(`[TIMING] handleSubmitToServer - Current timing offset: ${timingOffsetMs}ms`);
|
|
838
|
+
|
|
839
|
+
// Apply timing offset to the data before submission if needed
|
|
840
|
+
const dataToSubmit = timingOffsetMs !== 0
|
|
841
|
+
? applyOffsetToCorrectionData(data, timingOffsetMs)
|
|
842
|
+
: data
|
|
843
|
+
|
|
844
|
+
// Log some example timestamps after potential offset application
|
|
845
|
+
if (dataToSubmit.corrected_segments.length > 0) {
|
|
846
|
+
const firstSegment = dataToSubmit.corrected_segments[0];
|
|
847
|
+
console.log(`[TIMING] Submitting data - First segment id: ${firstSegment.id}`);
|
|
848
|
+
console.log(`[TIMING] - start_time: ${firstSegment.start_time}, end_time: ${firstSegment.end_time}`);
|
|
849
|
+
|
|
850
|
+
if (firstSegment.words.length > 0) {
|
|
851
|
+
const firstWord = firstSegment.words[0];
|
|
852
|
+
console.log(`[TIMING] - first word "${firstWord.text}" time: ${firstWord.start_time} -> ${firstWord.end_time}`);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
await apiClient.submitCorrections(dataToSubmit)
|
|
857
|
+
|
|
858
|
+
// Submit annotations if any were collected
|
|
859
|
+
if (annotations.length > 0) {
|
|
860
|
+
console.log(`Submitting ${annotations.length} annotations...`)
|
|
861
|
+
try {
|
|
862
|
+
await apiClient.submitAnnotations(annotations)
|
|
863
|
+
console.log('Annotations submitted successfully')
|
|
864
|
+
} catch (error) {
|
|
865
|
+
console.error('Failed to submit annotations:', error)
|
|
866
|
+
// Don't block the main submission if annotations fail
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
setIsReviewComplete(true)
|
|
871
|
+
setIsReviewModalOpen(false)
|
|
872
|
+
|
|
873
|
+
// Close the browser tab
|
|
874
|
+
window.close()
|
|
875
|
+
} catch (error) {
|
|
876
|
+
console.error('Failed to submit corrections:', error)
|
|
877
|
+
alert('Failed to submit corrections. Please try again.')
|
|
878
|
+
}
|
|
879
|
+
}, [apiClient, data, timingOffsetMs, annotations])
|
|
880
|
+
|
|
881
|
+
// Update play segment handler
|
|
882
|
+
const handlePlaySegment = useCallback((startTime: number) => {
|
|
883
|
+
if (window.seekAndPlayAudio) {
|
|
884
|
+
// Apply the timing offset to the start time
|
|
885
|
+
const adjustedStartTime = timingOffsetMs !== 0
|
|
886
|
+
? startTime + (timingOffsetMs / 1000)
|
|
887
|
+
: startTime;
|
|
888
|
+
|
|
889
|
+
window.seekAndPlayAudio(adjustedStartTime)
|
|
890
|
+
}
|
|
891
|
+
}, [timingOffsetMs])
|
|
892
|
+
|
|
893
|
+
const handleResetCorrections = useCallback(() => {
|
|
894
|
+
if (window.confirm('Are you sure you want to reset all corrections? This cannot be undone.')) {
|
|
895
|
+
clearSavedData(initialData)
|
|
896
|
+
// Reset history to the original initial data
|
|
897
|
+
setHistory([JSON.parse(JSON.stringify(initialData))])
|
|
898
|
+
setHistoryIndex(0)
|
|
899
|
+
setModalContent(null)
|
|
900
|
+
setFlashingType(null)
|
|
901
|
+
setHighlightInfo(null)
|
|
902
|
+
setInteractionMode('edit')
|
|
903
|
+
}
|
|
904
|
+
}, [initialData])
|
|
905
|
+
|
|
906
|
+
const handleAddSegment = useCallback((beforeIndex: number) => {
|
|
907
|
+
const newData = addSegmentBefore(data, beforeIndex)
|
|
908
|
+
updateDataWithHistory(newData, 'add segment')
|
|
909
|
+
}, [data, updateDataWithHistory])
|
|
910
|
+
|
|
911
|
+
const handleSplitSegment = useCallback((segmentIndex: number, afterWordIndex: number) => {
|
|
912
|
+
const newData = splitSegment(data, segmentIndex, afterWordIndex)
|
|
913
|
+
if (newData) {
|
|
914
|
+
updateDataWithHistory(newData, 'split segment')
|
|
915
|
+
setEditModalSegment(null)
|
|
916
|
+
}
|
|
917
|
+
}, [data, updateDataWithHistory])
|
|
918
|
+
|
|
919
|
+
const handleMergeSegment = useCallback((segmentIndex: number, mergeWithNext: boolean) => {
|
|
920
|
+
const newData = mergeSegment(data, segmentIndex, mergeWithNext)
|
|
921
|
+
updateDataWithHistory(newData, 'merge segment')
|
|
922
|
+
setEditModalSegment(null)
|
|
923
|
+
}, [data, updateDataWithHistory])
|
|
924
|
+
|
|
925
|
+
const handleHandlerToggle = useCallback(async (handler: string, enabled: boolean) => {
|
|
926
|
+
if (!apiClient) return
|
|
927
|
+
|
|
928
|
+
try {
|
|
929
|
+
setIsUpdatingHandlers(true);
|
|
930
|
+
|
|
931
|
+
// Get current enabled handlers
|
|
932
|
+
const currentEnabled = new Set(data.metadata.enabled_handlers || [])
|
|
933
|
+
|
|
934
|
+
// Update the set based on the toggle
|
|
935
|
+
if (enabled) {
|
|
936
|
+
currentEnabled.add(handler)
|
|
937
|
+
} else {
|
|
938
|
+
currentEnabled.delete(handler)
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Call API to update handlers
|
|
942
|
+
const newData = await apiClient.updateHandlers(Array.from(currentEnabled))
|
|
943
|
+
|
|
944
|
+
// Update local state with new correction data
|
|
945
|
+
// This API call returns the *entire* new state, so treat it as a single history step
|
|
946
|
+
updateDataWithHistory(newData, `toggle handler ${handler}`); // Update history
|
|
947
|
+
|
|
948
|
+
// Clear any existing modals or highlights
|
|
949
|
+
setModalContent(null)
|
|
950
|
+
setFlashingType(null)
|
|
951
|
+
setHighlightInfo(null)
|
|
952
|
+
|
|
953
|
+
// Flash the updated corrections
|
|
954
|
+
handleFlash('corrected')
|
|
955
|
+
} catch (error) {
|
|
956
|
+
console.error('Failed to update handlers:', error)
|
|
957
|
+
alert('Failed to update correction handlers. Please try again.')
|
|
958
|
+
} finally {
|
|
959
|
+
setIsUpdatingHandlers(false);
|
|
960
|
+
}
|
|
961
|
+
}, [apiClient, data.metadata.enabled_handlers, handleFlash, updateDataWithHistory])
|
|
962
|
+
|
|
963
|
+
const handleHandlerClick = useCallback((handler: string) => {
|
|
964
|
+
if (debugLog) {
|
|
965
|
+
console.log('Handler clicked:', handler);
|
|
966
|
+
}
|
|
967
|
+
setFlashingHandler(handler);
|
|
968
|
+
setFlashingType('handler');
|
|
969
|
+
if (debugLog) {
|
|
970
|
+
console.log('Set flashingHandler to:', handler);
|
|
971
|
+
console.log('Set flashingType to: handler');
|
|
972
|
+
}
|
|
973
|
+
// Clear the flash after a short delay
|
|
974
|
+
setTimeout(() => {
|
|
975
|
+
if (debugLog) {
|
|
976
|
+
console.log('Clearing flash state');
|
|
977
|
+
}
|
|
978
|
+
setFlashingHandler(null);
|
|
979
|
+
setFlashingType(null);
|
|
980
|
+
}, 1500);
|
|
981
|
+
}, []);
|
|
982
|
+
|
|
983
|
+
// Wrap setModalSpacebarHandler in useCallback
|
|
984
|
+
const handleSetModalSpacebarHandler = useCallback((handler: (() => (e: KeyboardEvent) => void) | undefined) => {
|
|
985
|
+
if (debugLog) {
|
|
986
|
+
console.log('LyricsAnalyzer - Setting modal handler:', {
|
|
987
|
+
hasHandler: !!handler
|
|
988
|
+
})
|
|
989
|
+
}
|
|
990
|
+
// Update the global modal handler
|
|
991
|
+
setModalHandler(handler ? handler() : undefined, !!handler)
|
|
992
|
+
}, [])
|
|
993
|
+
|
|
994
|
+
// Add new handler for adding lyrics
|
|
995
|
+
const handleAddLyrics = useCallback(async (source: string, lyrics: string) => {
|
|
996
|
+
if (!apiClient) return
|
|
997
|
+
|
|
998
|
+
try {
|
|
999
|
+
setIsAddingLyrics(true)
|
|
1000
|
+
const newData = await apiClient.addLyrics(source, lyrics)
|
|
1001
|
+
// This API call returns the *entire* new state
|
|
1002
|
+
updateDataWithHistory(newData, 'add lyrics'); // Update history
|
|
1003
|
+
} finally {
|
|
1004
|
+
setIsAddingLyrics(false)
|
|
1005
|
+
}
|
|
1006
|
+
}, [apiClient, updateDataWithHistory])
|
|
1007
|
+
|
|
1008
|
+
const handleFindReplace = (findText: string, replaceText: string, options: { caseSensitive: boolean, useRegex: boolean, fullTextMode: boolean }) => {
|
|
1009
|
+
const newData = findAndReplace(data, findText, replaceText, options)
|
|
1010
|
+
updateDataWithHistory(newData, 'find/replace'); // Update history
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// Add handler for Un-Correct All functionality
|
|
1014
|
+
const handleUnCorrectAll = useCallback(() => {
|
|
1015
|
+
if (!originalData.original_segments) {
|
|
1016
|
+
console.warn('No original segments available for un-correcting')
|
|
1017
|
+
return
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
if (window.confirm('Are you sure you want to revert all segments to their original transcribed state? This will undo all corrections made.')) {
|
|
1021
|
+
console.log('Un-Correct All: Reverting all segments to original transcribed state', {
|
|
1022
|
+
originalSegmentCount: originalData.original_segments.length,
|
|
1023
|
+
currentSegmentCount: data.corrected_segments.length
|
|
1024
|
+
})
|
|
1025
|
+
|
|
1026
|
+
// Create new data with original segments as corrected segments
|
|
1027
|
+
const newData: CorrectionData = {
|
|
1028
|
+
...data,
|
|
1029
|
+
corrected_segments: JSON.parse(JSON.stringify(originalData.original_segments))
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
updateDataWithHistory(newData, 'un-correct all segments')
|
|
1033
|
+
}
|
|
1034
|
+
}, [originalData.original_segments, data, updateDataWithHistory])
|
|
1035
|
+
|
|
1036
|
+
// Add handler for Replace All Lyrics functionality
|
|
1037
|
+
const handleReplaceAllLyrics = useCallback(() => {
|
|
1038
|
+
console.log('ReplaceAllLyrics - Opening modal')
|
|
1039
|
+
setIsReplaceAllLyricsModalOpen(true)
|
|
1040
|
+
}, [])
|
|
1041
|
+
|
|
1042
|
+
// Handle saving new segments from Replace All Lyrics
|
|
1043
|
+
const handleSaveReplaceAllLyrics = useCallback((newSegments: LyricsSegment[]) => {
|
|
1044
|
+
console.log('ReplaceAllLyrics - Saving new segments:', {
|
|
1045
|
+
segmentCount: newSegments.length,
|
|
1046
|
+
totalWords: newSegments.reduce((count, segment) => count + segment.words.length, 0)
|
|
1047
|
+
})
|
|
1048
|
+
|
|
1049
|
+
// Create new data with the replaced segments
|
|
1050
|
+
const newData = {
|
|
1051
|
+
...data,
|
|
1052
|
+
corrected_segments: newSegments
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
updateDataWithHistory(newData, 'replace all lyrics')
|
|
1056
|
+
setIsReplaceAllLyricsModalOpen(false)
|
|
1057
|
+
}, [data, updateDataWithHistory])
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
|
|
1061
|
+
// Undo/Redo handlers
|
|
1062
|
+
const handleUndo = useCallback(() => {
|
|
1063
|
+
if (historyIndex > 0) {
|
|
1064
|
+
const newIndex = historyIndex - 1;
|
|
1065
|
+
if (debugLog) {
|
|
1066
|
+
console.log(`[DEBUG] Undo: moving from index ${historyIndex} to ${newIndex}. History length: ${history.length}`);
|
|
1067
|
+
}
|
|
1068
|
+
setHistoryIndex(newIndex);
|
|
1069
|
+
} else {
|
|
1070
|
+
if (debugLog) {
|
|
1071
|
+
console.log(`[DEBUG] Undo: already at the beginning (index ${historyIndex})`);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}, [historyIndex, history])
|
|
1075
|
+
|
|
1076
|
+
const handleRedo = useCallback(() => {
|
|
1077
|
+
if (historyIndex < history.length - 1) {
|
|
1078
|
+
const newIndex = historyIndex + 1;
|
|
1079
|
+
if (debugLog) {
|
|
1080
|
+
console.log(`[DEBUG] Redo: moving from index ${historyIndex} to ${newIndex}. History length: ${history.length}`);
|
|
1081
|
+
}
|
|
1082
|
+
setHistoryIndex(newIndex);
|
|
1083
|
+
} else {
|
|
1084
|
+
if (debugLog) {
|
|
1085
|
+
console.log(`[DEBUG] Redo: already at the end (index ${historyIndex}, history length ${history.length})`);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}, [historyIndex, history])
|
|
1089
|
+
|
|
1090
|
+
// Determine if Undo/Redo is possible
|
|
1091
|
+
const canUndo = historyIndex > 0
|
|
1092
|
+
const canRedo = historyIndex < history.length - 1
|
|
1093
|
+
|
|
1094
|
+
// Memoize the metric click handlers
|
|
1095
|
+
const metricClickHandlers = useMemo(() => ({
|
|
1096
|
+
anchor: () => handleFlash('anchor'),
|
|
1097
|
+
corrected: () => handleFlash('corrected'),
|
|
1098
|
+
uncorrected: () => handleFlash('uncorrected')
|
|
1099
|
+
}), [handleFlash]);
|
|
1100
|
+
|
|
1101
|
+
// Determine if any modal is open to disable highlighting
|
|
1102
|
+
const isAnyModalOpenMemo = useMemo(() => isAnyModalOpen, [isAnyModalOpen]);
|
|
1103
|
+
|
|
1104
|
+
// For the TranscriptionView, we need to apply the timing offset when displaying
|
|
1105
|
+
const displayData = useMemo(() => {
|
|
1106
|
+
return timingOffsetMs !== 0
|
|
1107
|
+
? applyOffsetToCorrectionData(data, timingOffsetMs)
|
|
1108
|
+
: data;
|
|
1109
|
+
}, [data, timingOffsetMs]);
|
|
1110
|
+
|
|
1111
|
+
// Handler for opening the timing offset modal
|
|
1112
|
+
const handleOpenTimingOffsetModal = useCallback(() => {
|
|
1113
|
+
setIsTimingOffsetModalOpen(true)
|
|
1114
|
+
}, [])
|
|
1115
|
+
|
|
1116
|
+
// Handler for applying the timing offset
|
|
1117
|
+
const handleApplyTimingOffset = useCallback((offsetMs: number) => {
|
|
1118
|
+
// Only update if the offset has changed
|
|
1119
|
+
if (offsetMs !== timingOffsetMs) {
|
|
1120
|
+
console.log(`[TIMING] handleApplyTimingOffset: Changing offset from ${timingOffsetMs}ms to ${offsetMs}ms`);
|
|
1121
|
+
setTimingOffsetMs(offsetMs)
|
|
1122
|
+
|
|
1123
|
+
// If we're applying an offset, we don't need to update history
|
|
1124
|
+
// since we're not modifying the original data
|
|
1125
|
+
if (debugLog) {
|
|
1126
|
+
console.log(`[DEBUG] handleApplyTimingOffset: Setting offset to ${offsetMs}ms`);
|
|
1127
|
+
}
|
|
1128
|
+
} else {
|
|
1129
|
+
console.log(`[TIMING] handleApplyTimingOffset: Offset unchanged at ${offsetMs}ms`);
|
|
1130
|
+
}
|
|
1131
|
+
}, [timingOffsetMs])
|
|
1132
|
+
|
|
1133
|
+
// Add logging for timing offset changes
|
|
1134
|
+
useEffect(() => {
|
|
1135
|
+
console.log(`[TIMING] timingOffsetMs changed to: ${timingOffsetMs}ms`);
|
|
1136
|
+
}, [timingOffsetMs]);
|
|
1137
|
+
|
|
1138
|
+
return (
|
|
1139
|
+
<Box sx={{
|
|
1140
|
+
p: 1,
|
|
1141
|
+
pb: 3,
|
|
1142
|
+
maxWidth: '100%',
|
|
1143
|
+
overflowX: 'hidden'
|
|
1144
|
+
}}>
|
|
1145
|
+
<MemoizedHeader
|
|
1146
|
+
isReadOnly={isReadOnly}
|
|
1147
|
+
onFileLoad={onFileLoad}
|
|
1148
|
+
data={data}
|
|
1149
|
+
onMetricClick={metricClickHandlers}
|
|
1150
|
+
effectiveMode={effectiveMode}
|
|
1151
|
+
onModeChange={setInteractionMode}
|
|
1152
|
+
apiClient={apiClient}
|
|
1153
|
+
audioHash={audioHash}
|
|
1154
|
+
onTimeUpdate={setCurrentAudioTime}
|
|
1155
|
+
onHandlerToggle={handleHandlerToggle}
|
|
1156
|
+
isUpdatingHandlers={isUpdatingHandlers}
|
|
1157
|
+
onHandlerClick={handleHandlerClick}
|
|
1158
|
+
onFindReplace={() => setIsFindReplaceModalOpen(true)}
|
|
1159
|
+
onEditAll={handleReplaceAllLyrics}
|
|
1160
|
+
onTimingOffset={handleOpenTimingOffsetModal}
|
|
1161
|
+
timingOffsetMs={timingOffsetMs}
|
|
1162
|
+
onUndo={handleUndo}
|
|
1163
|
+
onRedo={handleRedo}
|
|
1164
|
+
canUndo={canUndo}
|
|
1165
|
+
canRedo={canRedo}
|
|
1166
|
+
onUnCorrectAll={handleUnCorrectAll}
|
|
1167
|
+
/>
|
|
1168
|
+
|
|
1169
|
+
<Grid container direction={isMobile ? 'column' : 'row'}>
|
|
1170
|
+
<Grid item xs={12} md={6}>
|
|
1171
|
+
<MemoizedTranscriptionView
|
|
1172
|
+
data={displayData}
|
|
1173
|
+
mode={effectiveMode}
|
|
1174
|
+
onElementClick={setModalContent}
|
|
1175
|
+
onWordClick={handleWordClick}
|
|
1176
|
+
flashingType={flashingType}
|
|
1177
|
+
flashingHandler={flashingHandler}
|
|
1178
|
+
highlightInfo={highlightInfo}
|
|
1179
|
+
onPlaySegment={handlePlaySegment}
|
|
1180
|
+
currentTime={currentAudioTime}
|
|
1181
|
+
anchors={data.anchor_sequences}
|
|
1182
|
+
disableHighlighting={isAnyModalOpenMemo}
|
|
1183
|
+
onDataChange={(updatedData) => {
|
|
1184
|
+
// Direct data change from TranscriptionView (e.g., drag-and-drop)
|
|
1185
|
+
// needs to update history
|
|
1186
|
+
updateDataWithHistory(updatedData, 'direct data change');
|
|
1187
|
+
}}
|
|
1188
|
+
/>
|
|
1189
|
+
{!isReadOnly && apiClient && (
|
|
1190
|
+
<Box sx={{
|
|
1191
|
+
mt: 2,
|
|
1192
|
+
mb: 3,
|
|
1193
|
+
display: 'flex',
|
|
1194
|
+
justifyContent: 'space-between',
|
|
1195
|
+
width: '100%'
|
|
1196
|
+
}}>
|
|
1197
|
+
<Button
|
|
1198
|
+
variant="outlined"
|
|
1199
|
+
color="warning"
|
|
1200
|
+
onClick={handleResetCorrections}
|
|
1201
|
+
startIcon={<RestoreFromTrash />}
|
|
1202
|
+
>
|
|
1203
|
+
Reset Corrections
|
|
1204
|
+
</Button>
|
|
1205
|
+
<Button
|
|
1206
|
+
variant="contained"
|
|
1207
|
+
onClick={handleFinishReview}
|
|
1208
|
+
disabled={isReviewComplete}
|
|
1209
|
+
endIcon={<OndemandVideo />}
|
|
1210
|
+
>
|
|
1211
|
+
{isReviewComplete ? 'Review Complete' : 'Preview Video'}
|
|
1212
|
+
</Button>
|
|
1213
|
+
</Box>
|
|
1214
|
+
)}
|
|
1215
|
+
</Grid>
|
|
1216
|
+
<Grid item xs={12} md={6}>
|
|
1217
|
+
<MemoizedReferenceView
|
|
1218
|
+
referenceSources={data.reference_lyrics}
|
|
1219
|
+
anchors={data.anchor_sequences}
|
|
1220
|
+
gaps={data.gap_sequences}
|
|
1221
|
+
mode={effectiveMode}
|
|
1222
|
+
onElementClick={setModalContent}
|
|
1223
|
+
onWordClick={handleWordClick}
|
|
1224
|
+
flashingType={flashingType}
|
|
1225
|
+
highlightInfo={highlightInfo}
|
|
1226
|
+
currentSource={currentSource}
|
|
1227
|
+
onSourceChange={setCurrentSource}
|
|
1228
|
+
corrected_segments={data.corrected_segments}
|
|
1229
|
+
corrections={data.corrections}
|
|
1230
|
+
onAddLyrics={() => setIsAddLyricsModalOpen(true)}
|
|
1231
|
+
/>
|
|
1232
|
+
</Grid>
|
|
1233
|
+
</Grid>
|
|
1234
|
+
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
<EditModal
|
|
1238
|
+
open={Boolean(editModalSegment)}
|
|
1239
|
+
onClose={() => {
|
|
1240
|
+
setEditModalSegment(null)
|
|
1241
|
+
handleSetModalSpacebarHandler(undefined)
|
|
1242
|
+
}}
|
|
1243
|
+
segment={editModalSegment?.segment ?
|
|
1244
|
+
timingOffsetMs !== 0 ?
|
|
1245
|
+
applyOffsetToSegment(editModalSegment.segment, timingOffsetMs) :
|
|
1246
|
+
editModalSegment.segment :
|
|
1247
|
+
null}
|
|
1248
|
+
segmentIndex={editModalSegment?.index ?? null}
|
|
1249
|
+
originalSegment={editModalSegment?.originalSegment ?
|
|
1250
|
+
timingOffsetMs !== 0 ?
|
|
1251
|
+
applyOffsetToSegment(editModalSegment.originalSegment, timingOffsetMs) :
|
|
1252
|
+
editModalSegment.originalSegment :
|
|
1253
|
+
null}
|
|
1254
|
+
onSave={handleUpdateSegment}
|
|
1255
|
+
onDelete={handleDeleteSegment}
|
|
1256
|
+
onAddSegment={handleAddSegment}
|
|
1257
|
+
onSplitSegment={handleSplitSegment}
|
|
1258
|
+
onMergeSegment={handleMergeSegment}
|
|
1259
|
+
onPlaySegment={handlePlaySegment}
|
|
1260
|
+
currentTime={currentAudioTime}
|
|
1261
|
+
setModalSpacebarHandler={handleSetModalSpacebarHandler}
|
|
1262
|
+
originalTranscribedSegment={
|
|
1263
|
+
editModalSegment?.segment && editModalSegment?.index !== null && originalData.original_segments
|
|
1264
|
+
? (() => {
|
|
1265
|
+
const origSegment = originalData.original_segments.find(
|
|
1266
|
+
(s: LyricsSegment) => s.id === editModalSegment.segment.id
|
|
1267
|
+
) || null;
|
|
1268
|
+
|
|
1269
|
+
return origSegment && timingOffsetMs !== 0
|
|
1270
|
+
? applyOffsetToSegment(origSegment, timingOffsetMs)
|
|
1271
|
+
: origSegment;
|
|
1272
|
+
})()
|
|
1273
|
+
: null
|
|
1274
|
+
}
|
|
1275
|
+
/>
|
|
1276
|
+
|
|
1277
|
+
<ReviewChangesModal
|
|
1278
|
+
open={isReviewModalOpen}
|
|
1279
|
+
onClose={() => setIsReviewModalOpen(false)}
|
|
1280
|
+
originalData={originalData}
|
|
1281
|
+
updatedData={data}
|
|
1282
|
+
onSubmit={handleSubmitToServer}
|
|
1283
|
+
apiClient={apiClient}
|
|
1284
|
+
setModalSpacebarHandler={handleSetModalSpacebarHandler}
|
|
1285
|
+
timingOffsetMs={timingOffsetMs}
|
|
1286
|
+
/>
|
|
1287
|
+
|
|
1288
|
+
<AddLyricsModal
|
|
1289
|
+
open={isAddLyricsModalOpen}
|
|
1290
|
+
onClose={() => setIsAddLyricsModalOpen(false)}
|
|
1291
|
+
onSubmit={handleAddLyrics}
|
|
1292
|
+
isSubmitting={isAddingLyrics}
|
|
1293
|
+
/>
|
|
1294
|
+
|
|
1295
|
+
<FindReplaceModal
|
|
1296
|
+
open={isFindReplaceModalOpen}
|
|
1297
|
+
onClose={() => setIsFindReplaceModalOpen(false)}
|
|
1298
|
+
onReplace={handleFindReplace}
|
|
1299
|
+
data={data}
|
|
1300
|
+
/>
|
|
1301
|
+
|
|
1302
|
+
<TimingOffsetModal
|
|
1303
|
+
open={isTimingOffsetModalOpen}
|
|
1304
|
+
onClose={() => setIsTimingOffsetModalOpen(false)}
|
|
1305
|
+
currentOffset={timingOffsetMs}
|
|
1306
|
+
onApply={handleApplyTimingOffset}
|
|
1307
|
+
/>
|
|
1308
|
+
|
|
1309
|
+
<ReplaceAllLyricsModal
|
|
1310
|
+
open={isReplaceAllLyricsModalOpen}
|
|
1311
|
+
onClose={() => setIsReplaceAllLyricsModalOpen(false)}
|
|
1312
|
+
onSave={handleSaveReplaceAllLyrics}
|
|
1313
|
+
onPlaySegment={handlePlaySegment}
|
|
1314
|
+
currentTime={currentAudioTime}
|
|
1315
|
+
setModalSpacebarHandler={handleSetModalSpacebarHandler}
|
|
1316
|
+
/>
|
|
1317
|
+
|
|
1318
|
+
{pendingAnnotation && (
|
|
1319
|
+
<CorrectionAnnotationModal
|
|
1320
|
+
open={isAnnotationModalOpen}
|
|
1321
|
+
onClose={() => {
|
|
1322
|
+
setIsAnnotationModalOpen(false)
|
|
1323
|
+
setPendingAnnotation(null)
|
|
1324
|
+
}}
|
|
1325
|
+
onSave={handleSaveAnnotation}
|
|
1326
|
+
onSkip={handleSkipAnnotation}
|
|
1327
|
+
originalText={pendingAnnotation.originalText}
|
|
1328
|
+
correctedText={pendingAnnotation.correctedText}
|
|
1329
|
+
wordIdsAffected={pendingAnnotation.wordIdsAffected}
|
|
1330
|
+
agenticProposal={undefined} // TODO: Pass agentic proposal if available
|
|
1331
|
+
referenceSources={Object.keys(data.reference_lyrics || {})}
|
|
1332
|
+
audioHash={audioHash}
|
|
1333
|
+
artist={data.metadata?.audio_filepath?.split('/').pop()?.split('.')[0] || 'Unknown'}
|
|
1334
|
+
title={data.metadata?.audio_filepath?.split('/').pop()?.split('.')[0] || 'Unknown'}
|
|
1335
|
+
sessionId={audioHash} // Use audio hash as session ID for now
|
|
1336
|
+
gapId={pendingAnnotation.gapId}
|
|
1337
|
+
/>
|
|
1338
|
+
)}
|
|
1339
|
+
|
|
1340
|
+
{selectedCorrection && (
|
|
1341
|
+
<CorrectionDetailCard
|
|
1342
|
+
open={correctionDetailOpen}
|
|
1343
|
+
onClose={() => {
|
|
1344
|
+
setCorrectionDetailOpen(false)
|
|
1345
|
+
setSelectedCorrection(null)
|
|
1346
|
+
}}
|
|
1347
|
+
originalWord={selectedCorrection.originalWord}
|
|
1348
|
+
correctedWord={selectedCorrection.correctedWord}
|
|
1349
|
+
category={selectedCorrection.category}
|
|
1350
|
+
confidence={selectedCorrection.confidence}
|
|
1351
|
+
reason={selectedCorrection.reason}
|
|
1352
|
+
handler={selectedCorrection.handler}
|
|
1353
|
+
source={selectedCorrection.source}
|
|
1354
|
+
onRevert={() => {
|
|
1355
|
+
handleRevertCorrection(selectedCorrection.wordId)
|
|
1356
|
+
setCorrectionDetailOpen(false)
|
|
1357
|
+
setSelectedCorrection(null)
|
|
1358
|
+
}}
|
|
1359
|
+
onEdit={() => {
|
|
1360
|
+
handleEditCorrection(selectedCorrection.wordId)
|
|
1361
|
+
setCorrectionDetailOpen(false)
|
|
1362
|
+
setSelectedCorrection(null)
|
|
1363
|
+
}}
|
|
1364
|
+
onAccept={() => {
|
|
1365
|
+
handleAcceptCorrection(selectedCorrection.wordId)
|
|
1366
|
+
setCorrectionDetailOpen(false)
|
|
1367
|
+
setSelectedCorrection(null)
|
|
1368
|
+
}}
|
|
1369
|
+
/>
|
|
1370
|
+
)}
|
|
1371
|
+
</Box>
|
|
1372
|
+
)
|
|
1373
|
+
}
|