lyrics-transcriber 0.68.0__py3-none-any.whl → 0.70.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.
- lyrics_transcriber/frontend/REPLACE_ALL_FUNCTIONALITY.md +210 -0
- lyrics_transcriber/frontend/package.json +1 -1
- lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +4 -2
- lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +257 -134
- lyrics_transcriber/frontend/src/components/Header.tsx +14 -0
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +57 -234
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +27 -3
- lyrics_transcriber/frontend/src/components/ReplaceAllLyricsModal.tsx +688 -0
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +29 -18
- lyrics_transcriber/frontend/src/hooks/useManualSync.ts +198 -30
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
- lyrics_transcriber/frontend/web_assets/assets/{index-D7BQUJXK.js → index-BV5ep1cr.js} +1020 -327
- lyrics_transcriber/frontend/web_assets/assets/index-BV5ep1cr.js.map +1 -0
- lyrics_transcriber/frontend/web_assets/index.html +1 -1
- {lyrics_transcriber-0.68.0.dist-info → lyrics_transcriber-0.70.0.dist-info}/METADATA +1 -1
- {lyrics_transcriber-0.68.0.dist-info → lyrics_transcriber-0.70.0.dist-info}/RECORD +19 -17
- lyrics_transcriber/frontend/web_assets/assets/index-D7BQUJXK.js.map +0 -1
- {lyrics_transcriber-0.68.0.dist-info → lyrics_transcriber-0.70.0.dist-info}/LICENSE +0 -0
- {lyrics_transcriber-0.68.0.dist-info → lyrics_transcriber-0.70.0.dist-info}/WHEEL +0 -0
- {lyrics_transcriber-0.68.0.dist-info → lyrics_transcriber-0.70.0.dist-info}/entry_points.txt +0 -0
@@ -16,6 +16,7 @@ import TranscriptionView from './TranscriptionView'
|
|
16
16
|
import { WordClickInfo, FlashType } from './shared/types'
|
17
17
|
import EditModal from './EditModal'
|
18
18
|
import ReviewChangesModal from './ReviewChangesModal'
|
19
|
+
import ReplaceAllLyricsModal from './ReplaceAllLyricsModal'
|
19
20
|
import {
|
20
21
|
addSegmentBefore,
|
21
22
|
splitSegment,
|
@@ -39,6 +40,8 @@ declare global {
|
|
39
40
|
interface Window {
|
40
41
|
toggleAudioPlayback?: () => void;
|
41
42
|
seekAndPlayAudio?: (startTime: number) => void;
|
43
|
+
getAudioDuration?: () => number;
|
44
|
+
isAudioPlaying?: boolean;
|
42
45
|
}
|
43
46
|
}
|
44
47
|
|
@@ -193,6 +196,7 @@ interface MemoizedHeaderProps {
|
|
193
196
|
onRedo: () => void
|
194
197
|
canUndo: boolean
|
195
198
|
canRedo: boolean
|
199
|
+
onUnCorrectAll: () => void
|
196
200
|
}
|
197
201
|
|
198
202
|
// Create a memoized Header component
|
@@ -216,7 +220,8 @@ const MemoizedHeader = memo(function MemoizedHeader({
|
|
216
220
|
onUndo,
|
217
221
|
onRedo,
|
218
222
|
canUndo,
|
219
|
-
canRedo
|
223
|
+
canRedo,
|
224
|
+
onUnCorrectAll
|
220
225
|
}: MemoizedHeaderProps) {
|
221
226
|
return (
|
222
227
|
<Header
|
@@ -240,6 +245,7 @@ const MemoizedHeader = memo(function MemoizedHeader({
|
|
240
245
|
onRedo={onRedo}
|
241
246
|
canUndo={canUndo}
|
242
247
|
canRedo={canRedo}
|
248
|
+
onUnCorrectAll={onUnCorrectAll}
|
243
249
|
/>
|
244
250
|
);
|
245
251
|
});
|
@@ -265,11 +271,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
265
271
|
index: number
|
266
272
|
originalSegment: LyricsSegment
|
267
273
|
} | null>(null)
|
268
|
-
const [
|
269
|
-
const [globalEditSegment, setGlobalEditSegment] = useState<LyricsSegment | null>(null)
|
270
|
-
const [originalGlobalSegment, setOriginalGlobalSegment] = useState<LyricsSegment | null>(null)
|
271
|
-
const [originalTranscribedGlobalSegment, setOriginalTranscribedGlobalSegment] = useState<LyricsSegment | null>(null)
|
272
|
-
const [isLoadingGlobalEdit, setIsLoadingGlobalEdit] = useState(false)
|
274
|
+
const [isReplaceAllLyricsModalOpen, setIsReplaceAllLyricsModalOpen] = useState(false)
|
273
275
|
const [isReviewModalOpen, setIsReviewModalOpen] = useState(false)
|
274
276
|
const [currentAudioTime, setCurrentAudioTime] = useState(0)
|
275
277
|
const [isUpdatingHandlers, setIsUpdatingHandlers] = useState(false)
|
@@ -397,11 +399,11 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
397
399
|
isReviewModalOpen ||
|
398
400
|
isAddLyricsModalOpen ||
|
399
401
|
isFindReplaceModalOpen ||
|
400
|
-
|
402
|
+
isReplaceAllLyricsModalOpen ||
|
401
403
|
isTimingOffsetModalOpen
|
402
404
|
)
|
403
405
|
setIsAnyModalOpen(modalOpen)
|
404
|
-
}, [modalContent, editModalSegment, isReviewModalOpen, isAddLyricsModalOpen, isFindReplaceModalOpen,
|
406
|
+
}, [modalContent, editModalSegment, isReviewModalOpen, isAddLyricsModalOpen, isFindReplaceModalOpen, isReplaceAllLyricsModalOpen, isTimingOffsetModalOpen])
|
405
407
|
|
406
408
|
// Calculate effective mode based on modifier key states
|
407
409
|
const effectiveMode = isCtrlPressed ? 'delete_word' : (isShiftPressed ? 'highlight' : interactionMode)
|
@@ -783,212 +785,54 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
783
785
|
updateDataWithHistory(newData, 'find/replace'); // Update history
|
784
786
|
}
|
785
787
|
|
786
|
-
// Add handler for
|
787
|
-
const
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
id: 'loading-placeholder',
|
793
|
-
words: [],
|
794
|
-
text: '',
|
795
|
-
start_time: 0,
|
796
|
-
end_time: 1
|
797
|
-
};
|
798
|
-
|
799
|
-
// Set placeholder segments first
|
800
|
-
setGlobalEditSegment(placeholderSegment);
|
801
|
-
setOriginalGlobalSegment(placeholderSegment);
|
802
|
-
|
803
|
-
// Show loading state
|
804
|
-
setIsLoadingGlobalEdit(true);
|
805
|
-
console.log('EditAll - Set loading state to true');
|
806
|
-
|
807
|
-
// Open the modal with placeholder data
|
808
|
-
setIsEditAllModalOpen(true);
|
809
|
-
console.log('EditAll - Set modal open to true');
|
810
|
-
|
811
|
-
// Use requestAnimationFrame to ensure the modal with loading state is rendered
|
812
|
-
// before doing the expensive operation
|
813
|
-
requestAnimationFrame(() => {
|
814
|
-
console.log('EditAll - Inside requestAnimationFrame');
|
815
|
-
|
816
|
-
// Use setTimeout to allow the modal to render before doing the expensive operation
|
817
|
-
setTimeout(() => {
|
818
|
-
console.log('EditAll - Inside setTimeout, starting data processing');
|
819
|
-
|
820
|
-
try {
|
821
|
-
console.time('EditAll - Data processing');
|
822
|
-
|
823
|
-
// Create a combined segment with all words from all segments
|
824
|
-
const allWords = data.corrected_segments.flatMap(segment => segment.words)
|
825
|
-
console.log(`EditAll - Collected ${allWords.length} words from all segments`);
|
826
|
-
|
827
|
-
// Sort words by start time to maintain chronological order
|
828
|
-
const sortedWords = [...allWords].sort((a, b) => {
|
829
|
-
const aTime = a.start_time ?? 0
|
830
|
-
const bTime = b.start_time ?? 0
|
831
|
-
return aTime - bTime
|
832
|
-
})
|
833
|
-
console.log('EditAll - Sorted words by start time');
|
834
|
-
|
835
|
-
// Create a global segment containing all words
|
836
|
-
const globalSegment: LyricsSegment = {
|
837
|
-
id: 'global-edit',
|
838
|
-
words: sortedWords,
|
839
|
-
text: sortedWords.map(w => w.text).join(' '),
|
840
|
-
start_time: sortedWords[0]?.start_time ?? null,
|
841
|
-
end_time: sortedWords[sortedWords.length - 1]?.end_time ?? null
|
842
|
-
}
|
843
|
-
console.log('EditAll - Created global segment');
|
844
|
-
|
845
|
-
// Store the original global segment for reset functionality
|
846
|
-
setGlobalEditSegment(globalSegment)
|
847
|
-
console.log('EditAll - Set global edit segment');
|
848
|
-
|
849
|
-
setOriginalGlobalSegment(JSON.parse(JSON.stringify(globalSegment)))
|
850
|
-
console.log('EditAll - Set original global segment');
|
851
|
-
|
852
|
-
// Create the original transcribed global segment for Un-Correct functionality
|
853
|
-
if (originalData.original_segments) {
|
854
|
-
console.log('EditAll - Processing original segments for Un-Correct functionality');
|
855
|
-
|
856
|
-
// Get all words from original segments
|
857
|
-
const originalWords = originalData.original_segments.flatMap((segment: LyricsSegment) => segment.words)
|
858
|
-
console.log(`EditAll - Collected ${originalWords.length} words from original segments`);
|
859
|
-
|
860
|
-
// Sort words by start time
|
861
|
-
const sortedOriginalWords = [...originalWords].sort((a, b) => {
|
862
|
-
const aTime = a.start_time ?? 0
|
863
|
-
const bTime = b.start_time ?? 0
|
864
|
-
return aTime - bTime
|
865
|
-
})
|
866
|
-
console.log('EditAll - Sorted original words by start time');
|
867
|
-
|
868
|
-
// Create the original transcribed global segment
|
869
|
-
const originalTranscribedGlobal: LyricsSegment = {
|
870
|
-
id: 'original-transcribed-global',
|
871
|
-
words: sortedOriginalWords,
|
872
|
-
text: sortedOriginalWords.map(w => w.text).join(' '),
|
873
|
-
start_time: sortedOriginalWords[0]?.start_time ?? null,
|
874
|
-
end_time: sortedOriginalWords[sortedOriginalWords.length - 1]?.end_time ?? null
|
875
|
-
}
|
876
|
-
console.log('EditAll - Created original transcribed global segment');
|
877
|
-
|
878
|
-
setOriginalTranscribedGlobalSegment(originalTranscribedGlobal)
|
879
|
-
console.log('EditAll - Set original transcribed global segment');
|
880
|
-
} else {
|
881
|
-
setOriginalTranscribedGlobalSegment(null)
|
882
|
-
console.log('EditAll - No original segments found, set original transcribed global segment to null');
|
883
|
-
}
|
884
|
-
|
885
|
-
console.timeEnd('EditAll - Data processing');
|
886
|
-
} catch (error) {
|
887
|
-
console.error('Error preparing global edit data:', error);
|
888
|
-
} finally {
|
889
|
-
// Clear loading state
|
890
|
-
console.log('EditAll - Finished processing, setting loading state to false');
|
891
|
-
setIsLoadingGlobalEdit(false);
|
892
|
-
}
|
893
|
-
}, 100); // Small delay to allow the modal to render
|
894
|
-
});
|
895
|
-
}, [data.corrected_segments, originalData.original_segments])
|
896
|
-
|
897
|
-
// Handle saving the global edit
|
898
|
-
const handleSaveGlobalEdit = useCallback((updatedSegment: LyricsSegment) => {
|
899
|
-
console.log('Global Edit - Saving with new approach:', {
|
900
|
-
updatedSegmentId: updatedSegment.id,
|
901
|
-
wordCount: updatedSegment.words.length,
|
902
|
-
originalSegmentCount: data.corrected_segments.length,
|
903
|
-
originalTotalWordCount: data.corrected_segments.reduce((count, segment) => count + segment.words.length, 0)
|
904
|
-
})
|
905
|
-
|
906
|
-
// Get the updated words from the global segment
|
907
|
-
const updatedWords = updatedSegment.words
|
908
|
-
|
909
|
-
// Create a new array of segments with the same structure as the original
|
910
|
-
const updatedSegments = []
|
911
|
-
let wordIndex = 0
|
912
|
-
|
913
|
-
// Distribute words to segments based on the original segment sizes
|
914
|
-
for (const segment of data.corrected_segments) {
|
915
|
-
const originalWordCount = segment.words.length
|
788
|
+
// Add handler for Un-Correct All functionality
|
789
|
+
const handleUnCorrectAll = useCallback(() => {
|
790
|
+
if (!originalData.original_segments) {
|
791
|
+
console.warn('No original segments available for un-correcting')
|
792
|
+
return
|
793
|
+
}
|
916
794
|
|
917
|
-
|
918
|
-
|
919
|
-
|
795
|
+
if (window.confirm('Are you sure you want to revert all segments to their original transcribed state? This will undo all corrections made.')) {
|
796
|
+
console.log('Un-Correct All: Reverting all segments to original transcribed state', {
|
797
|
+
originalSegmentCount: originalData.original_segments.length,
|
798
|
+
currentSegmentCount: data.corrected_segments.length
|
799
|
+
})
|
920
800
|
|
921
|
-
|
922
|
-
|
801
|
+
// Create new data with original segments as corrected segments
|
802
|
+
const newData: CorrectionData = {
|
803
|
+
...data,
|
804
|
+
corrected_segments: JSON.parse(JSON.stringify(originalData.original_segments))
|
923
805
|
}
|
924
806
|
|
925
|
-
|
926
|
-
wordIndex = endIndex
|
927
|
-
|
928
|
-
// If we have words for this segment, create an updated segment
|
929
|
-
if (segmentWords.length > 0) {
|
930
|
-
// Recalculate segment start and end times
|
931
|
-
const validStartTimes = segmentWords.map(w => w.start_time).filter((t): t is number => t !== null)
|
932
|
-
const validEndTimes = segmentWords.map(w => w.end_time).filter((t): t is number => t !== null)
|
933
|
-
|
934
|
-
const segmentStartTime = validStartTimes.length > 0 ? Math.min(...validStartTimes) : null
|
935
|
-
const segmentEndTime = validEndTimes.length > 0 ? Math.max(...validEndTimes) : null
|
936
|
-
|
937
|
-
// Create the updated segment
|
938
|
-
updatedSegments.push({
|
939
|
-
...segment,
|
940
|
-
words: segmentWords,
|
941
|
-
text: segmentWords.map(w => w.text).join(' '),
|
942
|
-
start_time: segmentStartTime,
|
943
|
-
end_time: segmentEndTime
|
944
|
-
})
|
945
|
-
}
|
807
|
+
updateDataWithHistory(newData, 'un-correct all segments')
|
946
808
|
}
|
809
|
+
}, [originalData.original_segments, data, updateDataWithHistory])
|
947
810
|
|
948
|
-
|
949
|
-
|
950
|
-
|
951
|
-
|
952
|
-
|
953
|
-
// Combine the remaining words with the last segment
|
954
|
-
const combinedWords = [...lastSegment.words, ...remainingWords]
|
955
|
-
|
956
|
-
// Recalculate segment start and end times
|
957
|
-
const validStartTimes = combinedWords.map(w => w.start_time).filter((t): t is number => t !== null)
|
958
|
-
const validEndTimes = combinedWords.map(w => w.end_time).filter((t): t is number => t !== null)
|
959
|
-
|
960
|
-
const segmentStartTime = validStartTimes.length > 0 ? Math.min(...validStartTimes) : null
|
961
|
-
const segmentEndTime = validEndTimes.length > 0 ? Math.max(...validEndTimes) : null
|
962
|
-
|
963
|
-
// Update the last segment
|
964
|
-
updatedSegments[updatedSegments.length - 1] = {
|
965
|
-
...lastSegment,
|
966
|
-
words: combinedWords,
|
967
|
-
text: combinedWords.map(w => w.text).join(' '),
|
968
|
-
start_time: segmentStartTime,
|
969
|
-
end_time: segmentEndTime
|
970
|
-
}
|
971
|
-
}
|
811
|
+
// Add handler for Replace All Lyrics functionality
|
812
|
+
const handleReplaceAllLyrics = useCallback(() => {
|
813
|
+
console.log('ReplaceAllLyrics - Opening modal')
|
814
|
+
setIsReplaceAllLyricsModalOpen(true)
|
815
|
+
}, [])
|
972
816
|
|
973
|
-
|
974
|
-
|
975
|
-
|
976
|
-
|
977
|
-
|
817
|
+
// Handle saving new segments from Replace All Lyrics
|
818
|
+
const handleSaveReplaceAllLyrics = useCallback((newSegments: LyricsSegment[]) => {
|
819
|
+
console.log('ReplaceAllLyrics - Saving new segments:', {
|
820
|
+
segmentCount: newSegments.length,
|
821
|
+
totalWords: newSegments.reduce((count, segment) => count + segment.words.length, 0)
|
978
822
|
})
|
979
823
|
|
980
|
-
//
|
824
|
+
// Create new data with the replaced segments
|
981
825
|
const newData = {
|
982
826
|
...data,
|
983
|
-
corrected_segments:
|
984
|
-
}
|
985
|
-
|
986
|
-
|
987
|
-
|
988
|
-
setIsEditAllModalOpen(false)
|
989
|
-
setGlobalEditSegment(null)
|
827
|
+
corrected_segments: newSegments
|
828
|
+
}
|
829
|
+
|
830
|
+
updateDataWithHistory(newData, 'replace all lyrics')
|
831
|
+
setIsReplaceAllLyricsModalOpen(false)
|
990
832
|
}, [data, updateDataWithHistory])
|
991
833
|
|
834
|
+
|
835
|
+
|
992
836
|
// Undo/Redo handlers
|
993
837
|
const handleUndo = useCallback(() => {
|
994
838
|
if (historyIndex > 0) {
|
@@ -1087,13 +931,14 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
1087
931
|
isUpdatingHandlers={isUpdatingHandlers}
|
1088
932
|
onHandlerClick={handleHandlerClick}
|
1089
933
|
onFindReplace={() => setIsFindReplaceModalOpen(true)}
|
1090
|
-
onEditAll={
|
934
|
+
onEditAll={handleReplaceAllLyrics}
|
1091
935
|
onTimingOffset={handleOpenTimingOffsetModal}
|
1092
936
|
timingOffsetMs={timingOffsetMs}
|
1093
937
|
onUndo={handleUndo}
|
1094
938
|
onRedo={handleRedo}
|
1095
939
|
canUndo={canUndo}
|
1096
940
|
canRedo={canRedo}
|
941
|
+
onUnCorrectAll={handleUnCorrectAll}
|
1097
942
|
/>
|
1098
943
|
|
1099
944
|
<Grid container direction={isMobile ? 'column' : 'row'}>
|
@@ -1162,38 +1007,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
1162
1007
|
</Grid>
|
1163
1008
|
</Grid>
|
1164
1009
|
|
1165
|
-
|
1166
|
-
open={isEditAllModalOpen}
|
1167
|
-
onClose={() => {
|
1168
|
-
setIsEditAllModalOpen(false)
|
1169
|
-
setGlobalEditSegment(null)
|
1170
|
-
setOriginalGlobalSegment(null)
|
1171
|
-
setOriginalTranscribedGlobalSegment(null)
|
1172
|
-
handleSetModalSpacebarHandler(undefined)
|
1173
|
-
}}
|
1174
|
-
segment={globalEditSegment ?
|
1175
|
-
timingOffsetMs !== 0 ?
|
1176
|
-
applyOffsetToSegment(globalEditSegment, timingOffsetMs) :
|
1177
|
-
globalEditSegment :
|
1178
|
-
null}
|
1179
|
-
segmentIndex={null}
|
1180
|
-
originalSegment={originalGlobalSegment ?
|
1181
|
-
timingOffsetMs !== 0 ?
|
1182
|
-
applyOffsetToSegment(originalGlobalSegment, timingOffsetMs) :
|
1183
|
-
originalGlobalSegment :
|
1184
|
-
null}
|
1185
|
-
onSave={handleSaveGlobalEdit}
|
1186
|
-
onPlaySegment={handlePlaySegment}
|
1187
|
-
currentTime={currentAudioTime}
|
1188
|
-
setModalSpacebarHandler={handleSetModalSpacebarHandler}
|
1189
|
-
originalTranscribedSegment={originalTranscribedGlobalSegment ?
|
1190
|
-
timingOffsetMs !== 0 ?
|
1191
|
-
applyOffsetToSegment(originalTranscribedGlobalSegment, timingOffsetMs) :
|
1192
|
-
originalTranscribedGlobalSegment :
|
1193
|
-
null}
|
1194
|
-
isGlobal={true}
|
1195
|
-
isLoading={isLoadingGlobalEdit}
|
1196
|
-
/>
|
1010
|
+
|
1197
1011
|
|
1198
1012
|
<EditModal
|
1199
1013
|
open={Boolean(editModalSegment)}
|
@@ -1266,6 +1080,15 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
|
|
1266
1080
|
currentOffset={timingOffsetMs}
|
1267
1081
|
onApply={handleApplyTimingOffset}
|
1268
1082
|
/>
|
1083
|
+
|
1084
|
+
<ReplaceAllLyricsModal
|
1085
|
+
open={isReplaceAllLyricsModalOpen}
|
1086
|
+
onClose={() => setIsReplaceAllLyricsModalOpen(false)}
|
1087
|
+
onSave={handleSaveReplaceAllLyrics}
|
1088
|
+
onPlaySegment={handlePlaySegment}
|
1089
|
+
currentTime={currentAudioTime}
|
1090
|
+
setModalSpacebarHandler={handleSetModalSpacebarHandler}
|
1091
|
+
/>
|
1269
1092
|
</Box>
|
1270
1093
|
)
|
1271
1094
|
}
|
@@ -174,12 +174,36 @@ export default function ReferenceView({
|
|
174
174
|
navigator.clipboard.writeText(text);
|
175
175
|
};
|
176
176
|
|
177
|
+
// Helper function to copy all reference text from current source
|
178
|
+
const copyAllReferenceText = () => {
|
179
|
+
const allText = currentSourceSegments
|
180
|
+
.map(segment => segment.words.map(w => w.text).join(' '))
|
181
|
+
.join('\n');
|
182
|
+
copyToClipboard(allText);
|
183
|
+
};
|
184
|
+
|
177
185
|
return (
|
178
186
|
<Paper sx={{ p: 0.8, position: 'relative' }}>
|
179
187
|
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 0.5 }}>
|
180
|
-
<
|
181
|
-
|
182
|
-
|
188
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
189
|
+
<Typography variant="h6" sx={{ fontSize: '0.9rem', mb: 0 }}>
|
190
|
+
Reference Lyrics
|
191
|
+
</Typography>
|
192
|
+
<IconButton
|
193
|
+
size="small"
|
194
|
+
onClick={copyAllReferenceText}
|
195
|
+
sx={{
|
196
|
+
padding: '2px',
|
197
|
+
height: '20px',
|
198
|
+
width: '20px',
|
199
|
+
minHeight: '20px',
|
200
|
+
minWidth: '20px'
|
201
|
+
}}
|
202
|
+
title="Copy all reference lyrics"
|
203
|
+
>
|
204
|
+
<ContentCopyIcon sx={{ fontSize: '1rem' }} />
|
205
|
+
</IconButton>
|
206
|
+
</Box>
|
183
207
|
<SourceSelector
|
184
208
|
availableSources={availableSources}
|
185
209
|
currentSource={effectiveCurrentSource}
|