lyrics-transcriber 0.41.0__py3-none-any.whl → 0.42.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. lyrics_transcriber/core/controller.py +30 -52
  2. lyrics_transcriber/correction/anchor_sequence.py +325 -150
  3. lyrics_transcriber/correction/corrector.py +224 -107
  4. lyrics_transcriber/correction/handlers/base.py +28 -10
  5. lyrics_transcriber/correction/handlers/extend_anchor.py +47 -24
  6. lyrics_transcriber/correction/handlers/levenshtein.py +75 -33
  7. lyrics_transcriber/correction/handlers/llm.py +290 -0
  8. lyrics_transcriber/correction/handlers/no_space_punct_match.py +81 -36
  9. lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +46 -26
  10. lyrics_transcriber/correction/handlers/repeat.py +28 -11
  11. lyrics_transcriber/correction/handlers/sound_alike.py +68 -32
  12. lyrics_transcriber/correction/handlers/syllables_match.py +80 -30
  13. lyrics_transcriber/correction/handlers/word_count_match.py +36 -19
  14. lyrics_transcriber/correction/handlers/word_operations.py +68 -22
  15. lyrics_transcriber/correction/text_utils.py +3 -7
  16. lyrics_transcriber/frontend/.yarn/install-state.gz +0 -0
  17. lyrics_transcriber/frontend/.yarn/releases/yarn-4.6.0.cjs +934 -0
  18. lyrics_transcriber/frontend/.yarnrc.yml +3 -0
  19. lyrics_transcriber/frontend/dist/assets/{index-DKnNJHRK.js → index-coH8y7gV.js} +16284 -9032
  20. lyrics_transcriber/frontend/dist/assets/index-coH8y7gV.js.map +1 -0
  21. lyrics_transcriber/frontend/dist/index.html +1 -1
  22. lyrics_transcriber/frontend/package.json +6 -2
  23. lyrics_transcriber/frontend/src/App.tsx +18 -2
  24. lyrics_transcriber/frontend/src/api.ts +103 -6
  25. lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +7 -6
  26. lyrics_transcriber/frontend/src/components/DetailsModal.tsx +86 -59
  27. lyrics_transcriber/frontend/src/components/EditModal.tsx +93 -43
  28. lyrics_transcriber/frontend/src/components/FileUpload.tsx +2 -2
  29. lyrics_transcriber/frontend/src/components/Header.tsx +251 -0
  30. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +303 -265
  31. lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +117 -0
  32. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +125 -40
  33. lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +129 -115
  34. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +59 -78
  35. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +40 -16
  36. lyrics_transcriber/frontend/src/components/WordEditControls.tsx +4 -10
  37. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +137 -68
  38. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +1 -1
  39. lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +85 -115
  40. lyrics_transcriber/frontend/src/components/shared/types.js +2 -0
  41. lyrics_transcriber/frontend/src/components/shared/types.ts +15 -7
  42. lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +35 -0
  43. lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +78 -0
  44. lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +7 -7
  45. lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +121 -0
  46. lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +22 -0
  47. lyrics_transcriber/frontend/src/types.js +2 -0
  48. lyrics_transcriber/frontend/src/types.ts +70 -49
  49. lyrics_transcriber/frontend/src/validation.ts +132 -0
  50. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
  51. lyrics_transcriber/frontend/yarn.lock +3752 -0
  52. lyrics_transcriber/lyrics/base_lyrics_provider.py +75 -12
  53. lyrics_transcriber/lyrics/file_provider.py +6 -5
  54. lyrics_transcriber/lyrics/genius.py +5 -2
  55. lyrics_transcriber/lyrics/spotify.py +58 -21
  56. lyrics_transcriber/output/ass/config.py +16 -5
  57. lyrics_transcriber/output/cdg.py +1 -1
  58. lyrics_transcriber/output/generator.py +22 -8
  59. lyrics_transcriber/output/plain_text.py +15 -10
  60. lyrics_transcriber/output/segment_resizer.py +16 -3
  61. lyrics_transcriber/output/subtitles.py +27 -1
  62. lyrics_transcriber/output/video.py +107 -1
  63. lyrics_transcriber/review/__init__.py +0 -1
  64. lyrics_transcriber/review/server.py +337 -164
  65. lyrics_transcriber/transcribers/audioshake.py +3 -0
  66. lyrics_transcriber/transcribers/base_transcriber.py +11 -3
  67. lyrics_transcriber/transcribers/whisper.py +11 -1
  68. lyrics_transcriber/types.py +151 -105
  69. lyrics_transcriber/utils/word_utils.py +27 -0
  70. {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.42.0.dist-info}/METADATA +3 -1
  71. {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.42.0.dist-info}/RECORD +74 -61
  72. {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.42.0.dist-info}/WHEEL +1 -1
  73. lyrics_transcriber/frontend/dist/assets/index-DKnNJHRK.js.map +0 -1
  74. lyrics_transcriber/frontend/package-lock.json +0 -4260
  75. lyrics_transcriber/frontend/src/components/shared/utils/initializeDataWithIds.tsx +0 -202
  76. {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.42.0.dist-info}/LICENSE +0 -0
  77. {lyrics_transcriber-0.41.0.dist-info → lyrics_transcriber-0.42.0.dist-info}/entry_points.txt +0 -0
@@ -1,18 +1,8 @@
1
1
  import { useCallback } from 'react'
2
- import { AnchorSequence, GapSequence, InteractionMode } from '../../../types'
2
+ import { AnchorSequence, GapSequence, InteractionMode, WordCorrection } from '../../../types'
3
3
  import { ModalContent } from '../../LyricsAnalyzer'
4
4
  import { WordClickInfo } from '../types'
5
5
 
6
- // Define debug info type
7
- interface WordDebugInfo {
8
- wordSplitInfo?: {
9
- text: string
10
- startIndex: number
11
- endIndex: number
12
- }
13
- nearbyAnchors?: AnchorSequence[]
14
- }
15
-
16
6
  export interface UseWordClickProps {
17
7
  mode: InteractionMode
18
8
  onElementClick: (content: ModalContent) => void
@@ -20,103 +10,93 @@ export interface UseWordClickProps {
20
10
  isReference?: boolean
21
11
  currentSource?: string
22
12
  gaps?: GapSequence[]
13
+ anchors?: AnchorSequence[]
14
+ corrections?: WordCorrection[]
23
15
  }
24
16
 
25
17
  export function useWordClick({
26
18
  mode,
27
19
  onElementClick,
28
20
  onWordClick,
29
- isReference,
30
- currentSource,
31
- gaps = []
21
+ isReference = false,
22
+ currentSource = '',
23
+ gaps = [],
24
+ anchors = [],
25
+ corrections = []
32
26
  }: UseWordClickProps) {
33
27
  const handleWordClick = useCallback((
34
28
  word: string,
35
29
  wordId: string,
36
30
  anchor?: AnchorSequence,
37
- gap?: GapSequence,
38
- debugInfo?: WordDebugInfo
31
+ gap?: GapSequence
39
32
  ) => {
40
- console.log(JSON.stringify({
41
- debug: {
42
- clickedWord: word,
33
+ // Check if word belongs to anchor
34
+ const belongsToAnchor = anchor && (
35
+ isReference
36
+ ? anchor.reference_word_ids[currentSource]?.includes(wordId)
37
+ : anchor.transcribed_word_ids.includes(wordId)
38
+ )
39
+
40
+ // Find matching gap if not provided
41
+ const matchingGap = gap || gaps.find(g =>
42
+ g.transcribed_word_ids.includes(wordId) ||
43
+ Object.values(g.reference_word_ids).some(ids => ids.includes(wordId))
44
+ )
45
+
46
+ // Check if word belongs to gap - include both original and corrected words
47
+ const belongsToGap = matchingGap && (
48
+ isReference
49
+ ? matchingGap.reference_word_ids[currentSource]?.includes(wordId)
50
+ : (matchingGap.transcribed_word_ids.includes(wordId) ||
51
+ corrections.some(c =>
52
+ c.corrected_word_id === wordId ||
53
+ c.word_id === wordId
54
+ ))
55
+ )
56
+
57
+ // Debug info
58
+ console.log('Word Click Debug:', {
59
+ clickInfo: {
60
+ word,
43
61
  wordId,
44
62
  isReference,
45
63
  currentSource,
46
- wordInfo: debugInfo?.wordSplitInfo,
47
- nearbyAnchors: debugInfo?.nearbyAnchors,
48
- anchorInfo: anchor && {
49
- wordIds: anchor.word_ids,
50
- length: anchor.length,
51
- words: anchor.words,
52
- referenceWordIds: anchor.reference_word_ids,
53
- matchesWordId: isReference
54
- ? anchor.reference_word_ids[currentSource!]?.includes(wordId)
55
- : anchor.word_ids.includes(wordId)
56
- },
57
- gapInfo: gap && {
58
- wordIds: gap.word_ids,
59
- length: gap.length,
60
- words: gap.words,
61
- referenceWords: gap.reference_words,
62
- corrections: gap.corrections,
63
- matchesWordId: isReference
64
- ? gap.reference_words[currentSource!]?.includes(wordId)
65
- : gap.word_ids.includes(wordId)
66
- },
67
- belongsToAnchor: anchor && (
68
- isReference
69
- ? anchor.reference_word_ids[currentSource!]?.includes(wordId)
70
- : anchor.word_ids.includes(wordId)
71
- ),
72
- belongsToGap: gap && (
73
- isReference
74
- ? gap.corrections.some(c => c.word_id === wordId)
75
- : gap.word_ids.includes(wordId)
76
- ),
77
- wordIndexInGap: gap && gap.words.indexOf(word),
78
- hasMatchingCorrection: gap && gap.corrections.some(c => c.word_id === wordId)
64
+ mode
65
+ },
66
+ anchorInfo: anchor && {
67
+ id: anchor.id,
68
+ transcribedWordIds: anchor.transcribed_word_ids,
69
+ referenceWordIds: anchor.reference_word_ids,
70
+ belongsToAnchor
71
+ },
72
+ gapInfo: matchingGap && {
73
+ id: matchingGap.id,
74
+ transcribedWordIds: matchingGap.transcribed_word_ids,
75
+ referenceWordIds: matchingGap.reference_word_ids,
76
+ belongsToGap,
77
+ relatedCorrections: corrections.filter(c =>
78
+ matchingGap.transcribed_word_ids.includes(c.word_id) ||
79
+ c.corrected_word_id === wordId ||
80
+ c.word_id === wordId
81
+ )
79
82
  }
80
- }, null, 2))
83
+ })
81
84
 
82
85
  // For reference view clicks, find the corresponding gap
83
86
  if (isReference && currentSource) {
84
- // Extract position from wordId (e.g., "genius-word-3" -> 3)
85
- const position = parseInt(wordId.split('-').pop() || '', 10);
86
-
87
- // Find gap that has a correction matching this reference position
88
87
  const matchingGap = gaps?.find(g =>
89
- g.corrections.some(c => {
90
- const refPosition = c.reference_positions?.[currentSource];
91
- return typeof refPosition === 'number' && refPosition === position;
92
- })
93
- );
88
+ g.reference_word_ids[currentSource]?.includes(wordId)
89
+ )
94
90
 
95
91
  if (matchingGap) {
96
92
  console.log('Found matching gap for reference click:', {
97
- position,
93
+ wordId,
98
94
  gap: matchingGap
99
- });
100
- gap = matchingGap;
95
+ })
96
+ gap = matchingGap
101
97
  }
102
98
  }
103
99
 
104
- const belongsToAnchor = anchor && (
105
- isReference
106
- ? anchor.reference_word_ids[currentSource!]?.includes(wordId)
107
- : anchor.word_ids.includes(wordId)
108
- )
109
-
110
- const belongsToGap = gap && (
111
- isReference
112
- ? gap.corrections.some(c => {
113
- const refPosition = c.reference_positions?.[currentSource!];
114
- const clickedPosition = parseInt(wordId.split('-').pop() || '', 10);
115
- return typeof refPosition === 'number' && refPosition === clickedPosition;
116
- })
117
- : gap.word_ids.includes(wordId)
118
- )
119
-
120
100
  if (mode === 'highlight' || mode === 'edit') {
121
101
  if (belongsToAnchor && anchor) {
122
102
  onWordClick?.({
@@ -126,32 +106,22 @@ export function useWordClick({
126
106
  gap: undefined
127
107
  })
128
108
  } else if (belongsToGap && gap) {
129
- // Create highlight info that includes both transcription and reference IDs
130
- const referenceWords: Record<string, string[]> = {};
131
-
132
- // For each correction in the gap, add its reference positions
133
- gap.corrections.forEach(correction => {
134
- Object.entries(correction.reference_positions || {}).forEach(([source, position]) => {
135
- if (typeof position === 'number') {
136
- const refId = `${source}-word-${position}`;
137
- if (!referenceWords[source]) {
138
- referenceWords[source] = [];
139
- }
140
- if (!referenceWords[source].includes(refId)) {
141
- referenceWords[source].push(refId);
142
- }
143
- }
144
- });
145
- });
146
-
147
109
  onWordClick?.({
148
110
  word_id: wordId,
149
111
  type: 'gap',
150
112
  anchor: undefined,
151
- gap: {
152
- ...gap,
153
- reference_words: referenceWords // Use reference_words instead of reference_word_ids
154
- }
113
+ gap
114
+ })
115
+ } else if (corrections.some(c =>
116
+ (c.corrected_word_id === wordId || c.word_id === wordId) &&
117
+ gap?.transcribed_word_ids.includes(c.word_id)
118
+ )) {
119
+ // If the word is part of a correction, mark it as a gap
120
+ onWordClick?.({
121
+ word_id: wordId,
122
+ type: 'gap',
123
+ anchor: undefined,
124
+ gap
155
125
  })
156
126
  } else {
157
127
  onWordClick?.({
@@ -168,7 +138,8 @@ export function useWordClick({
168
138
  data: {
169
139
  ...anchor,
170
140
  wordId,
171
- word
141
+ word,
142
+ anchor_sequences: anchors
172
143
  }
173
144
  })
174
145
  } else if (belongsToGap && gap) {
@@ -177,33 +148,32 @@ export function useWordClick({
177
148
  data: {
178
149
  ...gap,
179
150
  wordId,
180
- word
151
+ word,
152
+ anchor_sequences: anchors
181
153
  }
182
154
  })
183
155
  } else if (!isReference) {
184
156
  // Create synthetic gap for non-sequence words (transcription view only)
185
157
  const syntheticGap: GapSequence = {
186
158
  id: `synthetic-${wordId}`,
187
- text: word,
188
- words: [word],
189
- word_ids: [wordId],
190
- length: 1,
191
- corrections: [],
192
- preceding_anchor: null,
193
- following_anchor: null,
194
- reference_words: {}
159
+ transcribed_word_ids: [wordId],
160
+ transcription_position: -1,
161
+ preceding_anchor_id: null,
162
+ following_anchor_id: null,
163
+ reference_word_ids: {}
195
164
  }
196
165
  onElementClick({
197
166
  type: 'gap',
198
167
  data: {
199
168
  ...syntheticGap,
200
169
  wordId,
201
- word
170
+ word,
171
+ anchor_sequences: anchors
202
172
  }
203
173
  })
204
174
  }
205
175
  }
206
- }, [mode, onWordClick, onElementClick, isReference, currentSource, gaps])
176
+ }, [mode, onWordClick, onElementClick, isReference, currentSource, gaps, anchors, corrections])
207
177
 
208
178
  return { handleWordClick }
209
179
  }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,8 +1,8 @@
1
- import { AnchorSequence, GapSequence, HighlightInfo, InteractionMode, LyricsData, LyricsSegment } from '../../types'
1
+ import { AnchorSequence, GapSequence, HighlightInfo, InteractionMode, CorrectionData, LyricsSegment, ReferenceSource, WordCorrection } from '../../types'
2
2
  import { ModalContent } from '../LyricsAnalyzer'
3
3
 
4
4
  // Add FlashType definition directly in shared types
5
- export type FlashType = 'anchor' | 'corrected' | 'uncorrected' | 'word' | null
5
+ export type FlashType = 'anchor' | 'corrected' | 'uncorrected' | 'word' | 'handler' | null
6
6
 
7
7
  // Common word click handling
8
8
  export interface WordClickInfo {
@@ -66,10 +66,17 @@ export interface TextSegmentProps extends BaseViewProps {
66
66
  }
67
67
 
68
68
  // View-specific props
69
- export interface TranscriptionViewProps extends BaseViewProps {
70
- data: LyricsData
69
+ export interface TranscriptionViewProps {
70
+ data: CorrectionData
71
+ onElementClick: (content: ModalContent) => void
72
+ onWordClick?: (info: WordClickInfo) => void
73
+ flashingType: FlashType
74
+ highlightInfo: HighlightInfo | null
75
+ mode: InteractionMode
71
76
  onPlaySegment?: (startTime: number) => void
72
77
  currentTime?: number
78
+ anchors?: AnchorSequence[]
79
+ flashingHandler?: string | null
73
80
  }
74
81
 
75
82
  // Add LinePosition type here since it's used in multiple places
@@ -81,12 +88,13 @@ export interface LinePosition {
81
88
 
82
89
  // Reference-specific props
83
90
  export interface ReferenceViewProps extends BaseViewProps {
84
- referenceTexts: Record<string, string>
85
- anchors: LyricsData['anchor_sequences']
86
- gaps: LyricsData['gap_sequences']
91
+ referenceSources: Record<string, ReferenceSource>
92
+ anchors: CorrectionData['anchor_sequences']
93
+ gaps: CorrectionData['gap_sequences']
87
94
  currentSource: string
88
95
  onSourceChange: (source: string) => void
89
96
  corrected_segments: LyricsSegment[]
97
+ corrections: WordCorrection[]
90
98
  }
91
99
 
92
100
  // Update HighlightedTextProps to include linePositions
@@ -0,0 +1,35 @@
1
+ type KeyboardState = {
2
+ setIsShiftPressed: (value: boolean) => void
3
+ setIsCtrlPressed: (value: boolean) => void
4
+ }
5
+
6
+ export const setupKeyboardHandlers = (state: KeyboardState) => {
7
+ const handleKeyDown = (e: KeyboardEvent) => {
8
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
9
+ return
10
+ }
11
+
12
+ if (e.key === 'Shift') {
13
+ state.setIsShiftPressed(true)
14
+ document.body.style.userSelect = 'none'
15
+ } else if (e.key === 'Meta') {
16
+ state.setIsCtrlPressed(true)
17
+ } else if (e.key === ' ' || e.code === 'Space') {
18
+ e.preventDefault()
19
+ if (window.toggleAudioPlayback) {
20
+ window.toggleAudioPlayback()
21
+ }
22
+ }
23
+ }
24
+
25
+ const handleKeyUp = (e: KeyboardEvent) => {
26
+ if (e.key === 'Shift') {
27
+ state.setIsShiftPressed(false)
28
+ document.body.style.userSelect = ''
29
+ } else if (e.key === 'Meta') {
30
+ state.setIsCtrlPressed(false)
31
+ }
32
+ }
33
+
34
+ return { handleKeyDown, handleKeyUp }
35
+ }
@@ -0,0 +1,78 @@
1
+ import { CorrectionData, LyricsSegment } from '../../../types'
2
+
3
+ // Change the key generation to use a hash of the first segment's text instead
4
+ export const generateStorageKey = (data: CorrectionData): string => {
5
+ const text = data.original_segments[0]?.text || ''
6
+ let hash = 0
7
+ for (let i = 0; i < text.length; i++) {
8
+ const char = text.charCodeAt(i)
9
+ hash = ((hash << 5) - hash) + char
10
+ hash = hash & hash // Convert to 32-bit integer
11
+ }
12
+ return `song_${hash}`
13
+ }
14
+
15
+ const stripIds = (obj: CorrectionData): LyricsSegment[] => {
16
+ const clone = JSON.parse(JSON.stringify(obj))
17
+ return clone.corrected_segments.map((segment: LyricsSegment) => {
18
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
19
+ const { id: _id, ...strippedSegment } = segment
20
+ return {
21
+ ...strippedSegment,
22
+ words: segment.words.map(word => {
23
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
24
+ const { id: _wordId, ...strippedWord } = word
25
+ return strippedWord
26
+ })
27
+ }
28
+ })
29
+ }
30
+
31
+ export const loadSavedData = (initialData: CorrectionData): CorrectionData | null => {
32
+ const storageKey = generateStorageKey(initialData)
33
+ const savedDataStr = localStorage.getItem('lyrics_analyzer_data')
34
+ const savedDataObj = savedDataStr ? JSON.parse(savedDataStr) : {}
35
+
36
+ if (savedDataObj[storageKey]) {
37
+ try {
38
+ const parsed = savedDataObj[storageKey]
39
+ // Compare first segment text instead of transcribed_text
40
+ if (parsed.original_segments[0]?.text === initialData.original_segments[0]?.text) {
41
+ const strippedSaved = stripIds(parsed)
42
+ const strippedInitial = stripIds(initialData)
43
+ const hasChanges = JSON.stringify(strippedSaved) !== JSON.stringify(strippedInitial)
44
+
45
+ if (hasChanges) {
46
+ return parsed
47
+ } else {
48
+ // Clean up storage if no changes
49
+ delete savedDataObj[storageKey]
50
+ localStorage.setItem('lyrics_analyzer_data', JSON.stringify(savedDataObj))
51
+ }
52
+ }
53
+ } catch (error) {
54
+ console.error('Failed to parse saved data:', error)
55
+ delete savedDataObj[storageKey]
56
+ localStorage.setItem('lyrics_analyzer_data', JSON.stringify(savedDataObj))
57
+ }
58
+ }
59
+ return null
60
+ }
61
+
62
+ export const saveData = (data: CorrectionData, initialData: CorrectionData): void => {
63
+ const storageKey = generateStorageKey(initialData)
64
+ const savedDataStr = localStorage.getItem('lyrics_analyzer_data')
65
+ const savedDataObj = savedDataStr ? JSON.parse(savedDataStr) : {}
66
+
67
+ savedDataObj[storageKey] = data
68
+ localStorage.setItem('lyrics_analyzer_data', JSON.stringify(savedDataObj))
69
+ }
70
+
71
+ export const clearSavedData = (data: CorrectionData): void => {
72
+ const storageKey = generateStorageKey(data)
73
+ const savedDataStr = localStorage.getItem('lyrics_analyzer_data')
74
+ const savedDataObj = savedDataStr ? JSON.parse(savedDataStr) : {}
75
+
76
+ delete savedDataObj[storageKey]
77
+ localStorage.setItem('lyrics_analyzer_data', JSON.stringify(savedDataObj))
78
+ }
@@ -1,9 +1,9 @@
1
- import { LyricsData, LyricsSegment } from '../../../types'
1
+ import { AnchorSequence, LyricsSegment } from '../../../types'
2
2
  import { LinePosition } from '../types'
3
3
 
4
4
  export function calculateReferenceLinePositions(
5
5
  corrected_segments: LyricsSegment[],
6
- anchors: LyricsData['anchor_sequences'],
6
+ anchors: AnchorSequence[],
7
7
  currentSource: string
8
8
  ): { linePositions: LinePosition[] } {
9
9
  const linePositions: LinePosition[] = []
@@ -11,9 +11,7 @@ export function calculateReferenceLinePositions(
11
11
 
12
12
  // First, find all anchor sequences that cover entire lines
13
13
  const fullLineAnchors = anchors?.map(anchor => {
14
- // Add null checks for anchor and reference_word_ids
15
- if (!anchor?.reference_word_ids?.[currentSource]) return null
16
-
14
+ // Check if we have reference word IDs for this source
17
15
  const referenceWordIds = anchor.reference_word_ids[currentSource]
18
16
  if (!referenceWordIds?.length) return null
19
17
 
@@ -23,8 +21,10 @@ export function calculateReferenceLinePositions(
23
21
  const wordIds = segment.words.map(w => w.id)
24
22
  if (!wordIds.length) return false
25
23
 
26
- // Check if all word IDs in this segment are part of the anchor
27
- return wordIds.every(id => anchor.word_ids?.includes(id))
24
+ // Check if all word IDs in this segment are part of the anchor's transcribed word IDs
25
+ return wordIds.every(id =>
26
+ anchor.transcribed_word_ids.includes(id)
27
+ )
28
28
  })
29
29
  }
30
30
  })?.filter((a): a is NonNullable<typeof a> => a !== null) ?? []
@@ -0,0 +1,121 @@
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
+ }
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });