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,360 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid'
|
|
2
|
+
import { CorrectionData, LyricsSegment } from '../../../types'
|
|
3
|
+
|
|
4
|
+
export const addSegmentBefore = (
|
|
5
|
+
data: CorrectionData,
|
|
6
|
+
beforeIndex: number
|
|
7
|
+
): CorrectionData => {
|
|
8
|
+
const newData = { ...data }
|
|
9
|
+
const beforeSegment = newData.corrected_segments[beforeIndex]
|
|
10
|
+
|
|
11
|
+
// Create new segment starting 1 second before the target segment
|
|
12
|
+
// Use 0 as default if start_time is null
|
|
13
|
+
const newStartTime = Math.max(0, (beforeSegment.start_time ?? 1) - 1)
|
|
14
|
+
const newEndTime = newStartTime + 1
|
|
15
|
+
|
|
16
|
+
const newSegment: LyricsSegment = {
|
|
17
|
+
id: nanoid(),
|
|
18
|
+
text: "REPLACE",
|
|
19
|
+
start_time: newStartTime,
|
|
20
|
+
end_time: newEndTime,
|
|
21
|
+
words: [{
|
|
22
|
+
id: nanoid(),
|
|
23
|
+
text: "REPLACE",
|
|
24
|
+
start_time: newStartTime,
|
|
25
|
+
end_time: newEndTime,
|
|
26
|
+
confidence: 1.0
|
|
27
|
+
}]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Insert the new segment before the current one
|
|
31
|
+
newData.corrected_segments.splice(beforeIndex, 0, newSegment)
|
|
32
|
+
|
|
33
|
+
return newData
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const splitSegment = (
|
|
37
|
+
data: CorrectionData,
|
|
38
|
+
segmentIndex: number,
|
|
39
|
+
afterWordIndex: number
|
|
40
|
+
): CorrectionData | null => {
|
|
41
|
+
const newData = { ...data }
|
|
42
|
+
const segment = newData.corrected_segments[segmentIndex]
|
|
43
|
+
|
|
44
|
+
// Split the words array
|
|
45
|
+
const firstHalfWords = segment.words.slice(0, afterWordIndex + 1)
|
|
46
|
+
const secondHalfWords = segment.words.slice(afterWordIndex + 1)
|
|
47
|
+
|
|
48
|
+
if (secondHalfWords.length === 0) return null // Nothing to split
|
|
49
|
+
|
|
50
|
+
const lastFirstWord = firstHalfWords[firstHalfWords.length - 1]
|
|
51
|
+
const firstSecondWord = secondHalfWords[0]
|
|
52
|
+
const lastSecondWord = secondHalfWords[secondHalfWords.length - 1]
|
|
53
|
+
|
|
54
|
+
// Create two segments from the split
|
|
55
|
+
const firstSegment: LyricsSegment = {
|
|
56
|
+
...segment,
|
|
57
|
+
words: firstHalfWords,
|
|
58
|
+
text: firstHalfWords.map(w => w.text).join(' '),
|
|
59
|
+
end_time: lastFirstWord.end_time ?? null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const secondSegment: LyricsSegment = {
|
|
63
|
+
id: nanoid(),
|
|
64
|
+
words: secondHalfWords,
|
|
65
|
+
text: secondHalfWords.map(w => w.text).join(' '),
|
|
66
|
+
start_time: firstSecondWord.start_time ?? null,
|
|
67
|
+
end_time: lastSecondWord.end_time ?? null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Replace the original segment with the two new segments
|
|
71
|
+
newData.corrected_segments.splice(segmentIndex, 1, firstSegment, secondSegment)
|
|
72
|
+
|
|
73
|
+
return newData
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const deleteSegment = (
|
|
77
|
+
data: CorrectionData,
|
|
78
|
+
segmentIndex: number
|
|
79
|
+
): CorrectionData => {
|
|
80
|
+
const newData = { ...data }
|
|
81
|
+
const deletedSegment = newData.corrected_segments[segmentIndex]
|
|
82
|
+
|
|
83
|
+
// Remove segment
|
|
84
|
+
newData.corrected_segments = newData.corrected_segments.filter((_, index) => index !== segmentIndex)
|
|
85
|
+
|
|
86
|
+
// Update anchor sequences to remove references to deleted words
|
|
87
|
+
newData.anchor_sequences = newData.anchor_sequences.map(anchor => ({
|
|
88
|
+
...anchor,
|
|
89
|
+
transcribed_word_ids: anchor.transcribed_word_ids.filter(wordId =>
|
|
90
|
+
!deletedSegment.words.some(deletedWord => deletedWord.id === wordId)
|
|
91
|
+
)
|
|
92
|
+
}))
|
|
93
|
+
|
|
94
|
+
// Update gap sequences to remove references to deleted words
|
|
95
|
+
newData.gap_sequences = newData.gap_sequences.map(gap => ({
|
|
96
|
+
...gap,
|
|
97
|
+
transcribed_word_ids: gap.transcribed_word_ids.filter(wordId =>
|
|
98
|
+
!deletedSegment.words.some(deletedWord => deletedWord.id === wordId)
|
|
99
|
+
)
|
|
100
|
+
}))
|
|
101
|
+
|
|
102
|
+
return newData
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const updateSegment = (
|
|
106
|
+
data: CorrectionData,
|
|
107
|
+
segmentIndex: number,
|
|
108
|
+
updatedSegment: LyricsSegment
|
|
109
|
+
): CorrectionData => {
|
|
110
|
+
const newData = { ...data }
|
|
111
|
+
|
|
112
|
+
// Ensure new words have IDs
|
|
113
|
+
updatedSegment.words = updatedSegment.words.map(word => ({
|
|
114
|
+
...word,
|
|
115
|
+
id: word.id || nanoid()
|
|
116
|
+
}))
|
|
117
|
+
|
|
118
|
+
newData.corrected_segments[segmentIndex] = updatedSegment
|
|
119
|
+
|
|
120
|
+
return newData
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function mergeSegment(data: CorrectionData, segmentIndex: number, mergeWithNext: boolean): CorrectionData {
|
|
124
|
+
const segments = [...data.corrected_segments]
|
|
125
|
+
const targetIndex = mergeWithNext ? segmentIndex + 1 : segmentIndex - 1
|
|
126
|
+
|
|
127
|
+
// Check if target segment exists
|
|
128
|
+
if (targetIndex < 0 || targetIndex >= segments.length) {
|
|
129
|
+
return data
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const baseSegment = segments[segmentIndex]
|
|
133
|
+
const targetSegment = segments[targetIndex]
|
|
134
|
+
|
|
135
|
+
// Create merged segment
|
|
136
|
+
const mergedSegment: LyricsSegment = {
|
|
137
|
+
id: nanoid(),
|
|
138
|
+
words: mergeWithNext
|
|
139
|
+
? [...baseSegment.words, ...targetSegment.words]
|
|
140
|
+
: [...targetSegment.words, ...baseSegment.words],
|
|
141
|
+
text: mergeWithNext
|
|
142
|
+
? `${baseSegment.text} ${targetSegment.text}`
|
|
143
|
+
: `${targetSegment.text} ${baseSegment.text}`,
|
|
144
|
+
start_time: Math.min(
|
|
145
|
+
baseSegment.start_time ?? Infinity,
|
|
146
|
+
targetSegment.start_time ?? Infinity
|
|
147
|
+
),
|
|
148
|
+
end_time: Math.max(
|
|
149
|
+
baseSegment.end_time ?? -Infinity,
|
|
150
|
+
targetSegment.end_time ?? -Infinity
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Replace the two segments with the merged one
|
|
155
|
+
const minIndex = Math.min(segmentIndex, targetIndex)
|
|
156
|
+
segments.splice(minIndex, 2, mergedSegment)
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
...data,
|
|
160
|
+
corrected_segments: segments
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function findAndReplace(
|
|
165
|
+
data: CorrectionData,
|
|
166
|
+
findText: string,
|
|
167
|
+
replaceText: string,
|
|
168
|
+
options: { caseSensitive: boolean, useRegex: boolean, fullTextMode?: boolean } = {
|
|
169
|
+
caseSensitive: false,
|
|
170
|
+
useRegex: false,
|
|
171
|
+
fullTextMode: false
|
|
172
|
+
}
|
|
173
|
+
): CorrectionData {
|
|
174
|
+
const newData = { ...data }
|
|
175
|
+
|
|
176
|
+
// If full text mode is enabled, perform replacements across word boundaries
|
|
177
|
+
if (options.fullTextMode) {
|
|
178
|
+
newData.corrected_segments = data.corrected_segments.map(segment => {
|
|
179
|
+
// Create a pattern for the full segment text
|
|
180
|
+
let pattern: RegExp
|
|
181
|
+
|
|
182
|
+
if (options.useRegex) {
|
|
183
|
+
pattern = new RegExp(findText, options.caseSensitive ? 'g' : 'gi')
|
|
184
|
+
} else {
|
|
185
|
+
const escapedFindText = findText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
186
|
+
pattern = new RegExp(escapedFindText, options.caseSensitive ? 'g' : 'gi')
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Get the full segment text
|
|
190
|
+
const segmentText = segment.text
|
|
191
|
+
|
|
192
|
+
// If no matches, return the segment unchanged
|
|
193
|
+
if (!pattern.test(segmentText)) {
|
|
194
|
+
return segment
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Reset pattern for replacement
|
|
198
|
+
pattern.lastIndex = 0
|
|
199
|
+
|
|
200
|
+
// Replace in the full segment text
|
|
201
|
+
const newSegmentText = segmentText.replace(pattern, replaceText)
|
|
202
|
+
|
|
203
|
+
// Split the new text into words
|
|
204
|
+
const newWordTexts = newSegmentText.trim().split(/\s+/).filter(text => text.length > 0)
|
|
205
|
+
|
|
206
|
+
// Create new word objects
|
|
207
|
+
// We'll try to preserve original word IDs and timing info where possible
|
|
208
|
+
const newWords = []
|
|
209
|
+
|
|
210
|
+
// If we have the same number of words, we can preserve IDs and timing
|
|
211
|
+
if (newWordTexts.length === segment.words.length) {
|
|
212
|
+
for (let i = 0; i < newWordTexts.length; i++) {
|
|
213
|
+
newWords.push({
|
|
214
|
+
...segment.words[i],
|
|
215
|
+
text: newWordTexts[i]
|
|
216
|
+
})
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// If we have fewer words than before, some words were removed
|
|
220
|
+
else if (newWordTexts.length < segment.words.length) {
|
|
221
|
+
// Try to map new words to old words
|
|
222
|
+
let oldWordIndex = 0
|
|
223
|
+
for (let i = 0; i < newWordTexts.length; i++) {
|
|
224
|
+
// Find the next non-empty old word
|
|
225
|
+
while (oldWordIndex < segment.words.length &&
|
|
226
|
+
segment.words[oldWordIndex].text.trim() === '') {
|
|
227
|
+
oldWordIndex++
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (oldWordIndex < segment.words.length) {
|
|
231
|
+
newWords.push({
|
|
232
|
+
...segment.words[oldWordIndex],
|
|
233
|
+
text: newWordTexts[i]
|
|
234
|
+
})
|
|
235
|
+
oldWordIndex++
|
|
236
|
+
} else {
|
|
237
|
+
// If we run out of old words, create new ones
|
|
238
|
+
newWords.push({
|
|
239
|
+
id: nanoid(),
|
|
240
|
+
text: newWordTexts[i],
|
|
241
|
+
start_time: null,
|
|
242
|
+
end_time: null
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// If we have more words than before, some words were added
|
|
248
|
+
else {
|
|
249
|
+
// Try to preserve original words where possible
|
|
250
|
+
for (let i = 0; i < newWordTexts.length; i++) {
|
|
251
|
+
if (i < segment.words.length) {
|
|
252
|
+
newWords.push({
|
|
253
|
+
...segment.words[i],
|
|
254
|
+
text: newWordTexts[i]
|
|
255
|
+
})
|
|
256
|
+
} else {
|
|
257
|
+
// For new words, create new IDs
|
|
258
|
+
newWords.push({
|
|
259
|
+
id: nanoid(),
|
|
260
|
+
text: newWordTexts[i],
|
|
261
|
+
start_time: null,
|
|
262
|
+
end_time: null
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
...segment,
|
|
270
|
+
words: newWords,
|
|
271
|
+
text: newSegmentText
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
// Word-level replacement (original implementation)
|
|
276
|
+
else {
|
|
277
|
+
newData.corrected_segments = data.corrected_segments.map(segment => {
|
|
278
|
+
// Replace in each word
|
|
279
|
+
let newWords = segment.words.map(word => {
|
|
280
|
+
let pattern: RegExp
|
|
281
|
+
|
|
282
|
+
if (options.useRegex) {
|
|
283
|
+
// Create regex with or without case sensitivity
|
|
284
|
+
pattern = new RegExp(findText, options.caseSensitive ? 'g' : 'gi')
|
|
285
|
+
} else {
|
|
286
|
+
// Escape special regex characters for literal search
|
|
287
|
+
const escapedFindText = findText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
288
|
+
pattern = new RegExp(escapedFindText, options.caseSensitive ? 'g' : 'gi')
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
...word,
|
|
293
|
+
text: word.text.replace(pattern, replaceText)
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Filter out words that have become empty
|
|
298
|
+
newWords = newWords.filter(word => word.text.trim() !== '');
|
|
299
|
+
|
|
300
|
+
// Update segment text
|
|
301
|
+
return {
|
|
302
|
+
...segment,
|
|
303
|
+
words: newWords,
|
|
304
|
+
text: newWords.map(w => w.text).join(' ')
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Filter out segments that have no words left
|
|
310
|
+
newData.corrected_segments = newData.corrected_segments.filter(segment => segment.words.length > 0);
|
|
311
|
+
|
|
312
|
+
return newData
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Deletes a word from a segment in the correction data
|
|
317
|
+
* @param data The correction data
|
|
318
|
+
* @param wordId The ID of the word to delete
|
|
319
|
+
* @returns Updated correction data with the word removed
|
|
320
|
+
*/
|
|
321
|
+
export function deleteWord(
|
|
322
|
+
data: CorrectionData,
|
|
323
|
+
wordId: string
|
|
324
|
+
): CorrectionData {
|
|
325
|
+
// Find the segment containing this word
|
|
326
|
+
const segmentIndex = data.corrected_segments.findIndex(segment =>
|
|
327
|
+
segment.words.some(word => word.id === wordId)
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
if (segmentIndex === -1) {
|
|
331
|
+
// Word not found, return data unchanged
|
|
332
|
+
return data;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const segment = data.corrected_segments[segmentIndex];
|
|
336
|
+
const wordIndex = segment.words.findIndex(word => word.id === wordId);
|
|
337
|
+
|
|
338
|
+
if (wordIndex === -1) {
|
|
339
|
+
// Word not found in segment (shouldn't happen), return data unchanged
|
|
340
|
+
return data;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Create a new segment with the word removed
|
|
344
|
+
const updatedWords = segment.words.filter((_, index) => index !== wordIndex);
|
|
345
|
+
|
|
346
|
+
if (updatedWords.length > 0) {
|
|
347
|
+
// Update the segment with the word removed
|
|
348
|
+
const updatedSegment = {
|
|
349
|
+
...segment,
|
|
350
|
+
words: updatedWords,
|
|
351
|
+
text: updatedWords.map(w => w.text).join(' ')
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// Update the data
|
|
355
|
+
return updateSegment(data, segmentIndex, updatedSegment);
|
|
356
|
+
} else {
|
|
357
|
+
// If the segment would be empty, delete the entire segment
|
|
358
|
+
return deleteSegment(data, segmentIndex);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { LyricsSegment, Word, CorrectionData } from '../../../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Apply the timing offset to a single timestamp
|
|
5
|
+
* @param time The original timestamp in seconds
|
|
6
|
+
* @param offsetMs The offset in milliseconds
|
|
7
|
+
* @returns The adjusted timestamp in seconds
|
|
8
|
+
*/
|
|
9
|
+
export const applyOffsetToTime = (time: number | null, offsetMs: number): number | null => {
|
|
10
|
+
if (time === null) return null;
|
|
11
|
+
// Convert ms to seconds and add to the time
|
|
12
|
+
return time + (offsetMs / 1000);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Apply the timing offset to a word
|
|
17
|
+
* @param word The original word object
|
|
18
|
+
* @param offsetMs The offset in milliseconds
|
|
19
|
+
* @returns A new word object with adjusted timestamps
|
|
20
|
+
*/
|
|
21
|
+
export const applyOffsetToWord = (word: Word, offsetMs: number): Word => {
|
|
22
|
+
if (offsetMs === 0) return word;
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
...word,
|
|
26
|
+
start_time: applyOffsetToTime(word.start_time, offsetMs),
|
|
27
|
+
end_time: applyOffsetToTime(word.end_time, offsetMs)
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Apply the timing offset to all words in a segment
|
|
33
|
+
* Also updates the segment's start and end times based on the first and last word
|
|
34
|
+
* @param segment The original segment
|
|
35
|
+
* @param offsetMs The offset in milliseconds
|
|
36
|
+
* @returns A new segment with adjusted timestamps
|
|
37
|
+
*/
|
|
38
|
+
export const applyOffsetToSegment = (segment: LyricsSegment, offsetMs: number): LyricsSegment => {
|
|
39
|
+
if (offsetMs === 0) return segment;
|
|
40
|
+
|
|
41
|
+
const adjustedWords = segment.words.map(word => applyOffsetToWord(word, offsetMs));
|
|
42
|
+
|
|
43
|
+
// Update segment start/end times based on first/last word
|
|
44
|
+
const validStartTimes = adjustedWords.map(w => w.start_time).filter((t): t is number => t !== null);
|
|
45
|
+
const validEndTimes = adjustedWords.map(w => w.end_time).filter((t): t is number => t !== null);
|
|
46
|
+
|
|
47
|
+
const segmentStartTime = validStartTimes.length > 0 ? Math.min(...validStartTimes) : null;
|
|
48
|
+
const segmentEndTime = validEndTimes.length > 0 ? Math.max(...validEndTimes) : null;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
...segment,
|
|
52
|
+
words: adjustedWords,
|
|
53
|
+
start_time: segmentStartTime,
|
|
54
|
+
end_time: segmentEndTime
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Apply the timing offset to the entire correction data
|
|
60
|
+
* This creates a new data object with all timestamps adjusted by the offset
|
|
61
|
+
* @param data The original correction data
|
|
62
|
+
* @param offsetMs The offset in milliseconds
|
|
63
|
+
* @returns A new correction data object with all timestamps adjusted
|
|
64
|
+
*/
|
|
65
|
+
export const applyOffsetToCorrectionData = (data: CorrectionData, offsetMs: number): CorrectionData => {
|
|
66
|
+
console.log(`[TIMING] applyOffsetToCorrectionData called with offset: ${offsetMs}ms`);
|
|
67
|
+
|
|
68
|
+
if (offsetMs === 0) {
|
|
69
|
+
console.log('[TIMING] Offset is 0, returning original data');
|
|
70
|
+
return data;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Log some examples of original timestamps
|
|
74
|
+
if (data.corrected_segments.length > 0) {
|
|
75
|
+
const firstSegment = data.corrected_segments[0];
|
|
76
|
+
console.log(`[TIMING] First segment before offset - id: ${firstSegment.id}`);
|
|
77
|
+
console.log(`[TIMING] - start_time: ${firstSegment.start_time}, end_time: ${firstSegment.end_time}`);
|
|
78
|
+
|
|
79
|
+
if (firstSegment.words.length > 0) {
|
|
80
|
+
const firstWord = firstSegment.words[0];
|
|
81
|
+
const lastWord = firstSegment.words[firstSegment.words.length - 1];
|
|
82
|
+
console.log(`[TIMING] - first word "${firstWord.text}" time: ${firstWord.start_time} -> ${firstWord.end_time}`);
|
|
83
|
+
console.log(`[TIMING] - last word "${lastWord.text}" time: ${lastWord.start_time} -> ${lastWord.end_time}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const result = {
|
|
88
|
+
...data,
|
|
89
|
+
corrected_segments: data.corrected_segments.map(segment =>
|
|
90
|
+
applyOffsetToSegment(segment, offsetMs)
|
|
91
|
+
)
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Log some examples of adjusted timestamps
|
|
95
|
+
if (result.corrected_segments.length > 0) {
|
|
96
|
+
const firstSegment = result.corrected_segments[0];
|
|
97
|
+
console.log(`[TIMING] First segment AFTER offset - id: ${firstSegment.id}`);
|
|
98
|
+
console.log(`[TIMING] - start_time: ${firstSegment.start_time}, end_time: ${firstSegment.end_time}`);
|
|
99
|
+
|
|
100
|
+
if (firstSegment.words.length > 0) {
|
|
101
|
+
const firstWord = firstSegment.words[0];
|
|
102
|
+
const lastWord = firstSegment.words[firstSegment.words.length - 1];
|
|
103
|
+
console.log(`[TIMING] - first word "${firstWord.text}" time: ${firstWord.start_time} -> ${firstWord.end_time}`);
|
|
104
|
+
console.log(`[TIMING] - last word "${lastWord.text}" time: ${lastWord.start_time} -> ${lastWord.end_time}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log(`[TIMING] Finished applying offset of ${offsetMs}ms to data`);
|
|
109
|
+
return result;
|
|
110
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Word, LyricsSegment } from '../../../types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Find a Word object by its ID within an array of segments
|
|
5
|
+
*/
|
|
6
|
+
export function findWordById(segments: LyricsSegment[], wordId: string): Word | undefined {
|
|
7
|
+
for (const segment of segments) {
|
|
8
|
+
const word = segment.words.find(w => w.id === wordId)
|
|
9
|
+
if (word) return word
|
|
10
|
+
}
|
|
11
|
+
return undefined
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Convert an array of word IDs to their corresponding Word objects
|
|
16
|
+
* Filters out any IDs that don't match to valid words
|
|
17
|
+
*/
|
|
18
|
+
export function getWordsFromIds(segments: LyricsSegment[], wordIds: string[]): Word[] {
|
|
19
|
+
return wordIds
|
|
20
|
+
.map(id => findWordById(segments, id))
|
|
21
|
+
.filter((word): word is Word => word !== undefined)
|
|
22
|
+
}
|