lyrics-transcriber 0.42.0__py3-none-any.whl → 0.43.1__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 (22) hide show
  1. lyrics_transcriber/frontend/dist/assets/{index-coH8y7gV.js → index-D0Gr3Ep7.js} +283 -64
  2. lyrics_transcriber/frontend/dist/assets/index-D0Gr3Ep7.js.map +1 -0
  3. lyrics_transcriber/frontend/dist/index.html +1 -1
  4. lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +7 -0
  5. lyrics_transcriber/frontend/src/components/EditModal.tsx +198 -30
  6. lyrics_transcriber/frontend/src/components/Header.tsx +0 -2
  7. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +19 -3
  8. lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +4 -1
  9. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +54 -17
  10. lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +32 -2
  11. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +0 -1
  12. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +0 -3
  13. lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +33 -1
  14. lyrics_transcriber/frontend/src/types/global.d.ts +9 -0
  15. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
  16. lyrics_transcriber/output/video.py +18 -8
  17. {lyrics_transcriber-0.42.0.dist-info → lyrics_transcriber-0.43.1.dist-info}/METADATA +1 -1
  18. {lyrics_transcriber-0.42.0.dist-info → lyrics_transcriber-0.43.1.dist-info}/RECORD +21 -20
  19. lyrics_transcriber/frontend/dist/assets/index-coH8y7gV.js.map +0 -1
  20. {lyrics_transcriber-0.42.0.dist-info → lyrics_transcriber-0.43.1.dist-info}/LICENSE +0 -0
  21. {lyrics_transcriber-0.42.0.dist-info → lyrics_transcriber-0.43.1.dist-info}/WHEEL +0 -0
  22. {lyrics_transcriber-0.42.0.dist-info → lyrics_transcriber-0.43.1.dist-info}/entry_points.txt +0 -0
@@ -5,7 +5,7 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>Lyrics Transcriber Analyzer</title>
8
- <script type="module" crossorigin src="/assets/index-coH8y7gV.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-D0Gr3Ep7.js"></script>
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
@@ -33,16 +33,21 @@ export default function AudioPlayer({ apiClient, onTimeUpdate, audioHash }: Audi
33
33
  }
34
34
 
35
35
  audio.addEventListener('play', () => {
36
+ setIsPlaying(true)
37
+ window.isAudioPlaying = true
36
38
  updateTime()
37
39
  })
38
40
 
39
41
  audio.addEventListener('pause', () => {
42
+ setIsPlaying(false)
43
+ window.isAudioPlaying = false
40
44
  cancelAnimationFrame(animationFrameId)
41
45
  })
42
46
 
43
47
  audio.addEventListener('ended', () => {
44
48
  cancelAnimationFrame(animationFrameId)
45
49
  setIsPlaying(false)
50
+ window.isAudioPlaying = false
46
51
  setCurrentTime(0)
47
52
  })
48
53
 
@@ -55,6 +60,7 @@ export default function AudioPlayer({ apiClient, onTimeUpdate, audioHash }: Audi
55
60
  audio.pause()
56
61
  audio.src = ''
57
62
  audioRef.current = null
63
+ window.isAudioPlaying = false
58
64
  }
59
65
  }, [apiClient, onTimeUpdate, audioHash])
60
66
 
@@ -107,6 +113,7 @@ export default function AudioPlayer({ apiClient, onTimeUpdate, audioHash }: Audi
107
113
  useEffect(() => {
108
114
  if (!apiClient) return
109
115
 
116
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
110
117
  const win = window as any
111
118
  win.seekAndPlayAudio = seekAndPlay
112
119
  win.toggleAudioPlayback = togglePlayback
@@ -20,8 +20,10 @@ import RestoreIcon from '@mui/icons-material/RestoreFromTrash'
20
20
  import MoreVertIcon from '@mui/icons-material/MoreVert'
21
21
  import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh'
22
22
  import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline'
23
+ import CancelIcon from '@mui/icons-material/Cancel'
24
+ import StopIcon from '@mui/icons-material/Stop'
23
25
  import { LyricsSegment, Word } from '../types'
24
- import { useState, useEffect } from 'react'
26
+ import { useState, useEffect, useCallback } from 'react'
25
27
  import TimelineEditor from './TimelineEditor'
26
28
  import { nanoid } from 'nanoid'
27
29
 
@@ -37,6 +39,7 @@ interface EditModalProps {
37
39
  onDelete?: (segmentIndex: number) => void
38
40
  onAddSegment?: (segmentIndex: number) => void
39
41
  onSplitSegment?: (segmentIndex: number, afterWordIndex: number) => void
42
+ setModalSpacebarHandler: (handler: (() => (e: KeyboardEvent) => void) | undefined) => void
40
43
  }
41
44
 
42
45
  export default function EditModal({
@@ -51,26 +54,150 @@ export default function EditModal({
51
54
  onDelete,
52
55
  onAddSegment,
53
56
  onSplitSegment,
57
+ setModalSpacebarHandler,
54
58
  }: EditModalProps) {
59
+ // All useState hooks
55
60
  const [editedSegment, setEditedSegment] = useState<LyricsSegment | null>(segment)
56
61
  const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null)
57
62
  const [selectedWordIndex, setSelectedWordIndex] = useState<number | null>(null)
58
63
  const [replacementText, setReplacementText] = useState('')
64
+ const [isManualSyncing, setIsManualSyncing] = useState(false)
65
+ const [syncWordIndex, setSyncWordIndex] = useState<number>(-1)
66
+ const [isPlaying, setIsPlaying] = useState(false)
59
67
 
60
- // Reset edited segment when modal opens with new segment
68
+ // Define updateSegment first since other hooks depend on it
69
+ const updateSegment = useCallback((newWords: Word[]) => {
70
+ if (!editedSegment) return;
71
+
72
+ const validStartTimes = newWords.map(w => w.start_time).filter((t): t is number => t !== null)
73
+ const validEndTimes = newWords.map(w => w.end_time).filter((t): t is number => t !== null)
74
+
75
+ const segmentStartTime = validStartTimes.length > 0 ? Math.min(...validStartTimes) : null
76
+ const segmentEndTime = validEndTimes.length > 0 ? Math.max(...validEndTimes) : null
77
+
78
+ setEditedSegment({
79
+ ...editedSegment,
80
+ words: newWords,
81
+ text: newWords.map(w => w.text).join(' '),
82
+ start_time: segmentStartTime,
83
+ end_time: segmentEndTime
84
+ })
85
+ }, [editedSegment])
86
+
87
+ // Other useCallback hooks
88
+ const cleanupManualSync = useCallback(() => {
89
+ setIsManualSyncing(false)
90
+ setSyncWordIndex(-1)
91
+ }, [])
92
+
93
+ const handleClose = useCallback(() => {
94
+ cleanupManualSync()
95
+ onClose()
96
+ }, [onClose, cleanupManualSync])
97
+
98
+ // All useEffect hooks
61
99
  useEffect(() => {
62
100
  setEditedSegment(segment)
63
101
  }, [segment])
64
102
 
103
+ // Update the spacebar handler when modal state changes
104
+ useEffect(() => {
105
+ if (open) {
106
+ setModalSpacebarHandler(() => (e: KeyboardEvent) => {
107
+ e.preventDefault()
108
+ e.stopPropagation()
109
+
110
+ if (isManualSyncing && editedSegment) {
111
+ // Handle manual sync mode
112
+ if (syncWordIndex < editedSegment.words.length) {
113
+ const newWords = [...editedSegment.words]
114
+ const currentWord = newWords[syncWordIndex]
115
+ const prevWord = syncWordIndex > 0 ? newWords[syncWordIndex - 1] : null
116
+
117
+ currentWord.start_time = currentTime
118
+
119
+ if (prevWord) {
120
+ prevWord.end_time = currentTime - 0.01
121
+ }
122
+
123
+ if (syncWordIndex === editedSegment.words.length - 1) {
124
+ currentWord.end_time = editedSegment.end_time
125
+ setIsManualSyncing(false)
126
+ setSyncWordIndex(-1)
127
+ updateSegment(newWords)
128
+ } else {
129
+ setSyncWordIndex(syncWordIndex + 1)
130
+ updateSegment(newWords)
131
+ }
132
+ }
133
+ } else if (editedSegment && onPlaySegment) {
134
+ // Toggle segment playback when not in manual sync mode
135
+ const startTime = editedSegment.start_time ?? 0
136
+ const endTime = editedSegment.end_time ?? 0
137
+
138
+ if (currentTime >= startTime && currentTime <= endTime) {
139
+ if (window.toggleAudioPlayback) {
140
+ window.toggleAudioPlayback()
141
+ }
142
+ } else {
143
+ onPlaySegment(startTime)
144
+ }
145
+ }
146
+ })
147
+ } else {
148
+ setModalSpacebarHandler(undefined)
149
+ }
150
+
151
+ return () => {
152
+ setModalSpacebarHandler(undefined)
153
+ }
154
+ }, [
155
+ open,
156
+ isManualSyncing,
157
+ editedSegment,
158
+ syncWordIndex,
159
+ currentTime,
160
+ onPlaySegment,
161
+ updateSegment,
162
+ setModalSpacebarHandler
163
+ ])
164
+
165
+ // Auto-stop sync if we go past the end time
166
+ useEffect(() => {
167
+ if (!editedSegment) return
168
+
169
+ const endTime = editedSegment.end_time ?? 0
170
+
171
+ if (window.isAudioPlaying && currentTime > endTime) {
172
+ console.log('Stopping playback: current time exceeded end time')
173
+ window.toggleAudioPlayback?.()
174
+ setIsManualSyncing(false)
175
+ setSyncWordIndex(-1)
176
+ }
177
+
178
+ }, [isManualSyncing, editedSegment, currentTime, setSyncWordIndex])
179
+
180
+ // Update isPlaying when currentTime changes
181
+ useEffect(() => {
182
+ if (editedSegment) {
183
+ const startTime = editedSegment.start_time ?? 0
184
+ const endTime = editedSegment.end_time ?? 0
185
+ const isWithinSegment = currentTime >= startTime && currentTime <= endTime
186
+
187
+ // Only consider it playing if it's within the segment AND audio is actually playing
188
+ setIsPlaying(isWithinSegment && window.isAudioPlaying === true)
189
+ }
190
+ }, [currentTime, editedSegment])
191
+
65
192
  // Add a function to get safe time values
66
193
  const getSafeTimeRange = (segment: LyricsSegment | null) => {
67
194
  if (!segment) return { start: 0, end: 1 }; // Default 1-second range
68
-
69
195
  const start = segment.start_time ?? 0;
70
196
  const end = segment.end_time ?? (start + 1);
71
197
  return { start, end };
72
198
  }
73
199
 
200
+ // Early return after all hooks and function definitions
74
201
  if (!segment || segmentIndex === null || !editedSegment || !originalSegment) return null
75
202
 
76
203
  // Get safe time values for TimelineEditor
@@ -85,23 +212,6 @@ export default function EditModal({
85
212
  updateSegment(newWords)
86
213
  }
87
214
 
88
- const updateSegment = (newWords: Word[]) => {
89
- // Filter out null values before finding min/max
90
- const validStartTimes = newWords.map(w => w.start_time).filter((t): t is number => t !== null)
91
- const validEndTimes = newWords.map(w => w.end_time).filter((t): t is number => t !== null)
92
-
93
- const segmentStartTime = validStartTimes.length > 0 ? Math.min(...validStartTimes) : null
94
- const segmentEndTime = validEndTimes.length > 0 ? Math.max(...validEndTimes) : null
95
-
96
- setEditedSegment({
97
- ...editedSegment,
98
- words: newWords,
99
- text: newWords.map(w => w.text).join(' '),
100
- start_time: segmentStartTime,
101
- end_time: segmentEndTime
102
- })
103
- }
104
-
105
215
  const handleAddWord = (index?: number) => {
106
216
  const newWords = [...editedSegment.words]
107
217
  let newWord: Word
@@ -285,10 +395,42 @@ export default function EditModal({
285
395
  }
286
396
  }
287
397
 
398
+ // Add this new function to handle manual sync
399
+ const startManualSync = () => {
400
+ if (isManualSyncing) {
401
+ setIsManualSyncing(false)
402
+ setSyncWordIndex(-1)
403
+ return
404
+ }
405
+
406
+ if (!editedSegment || !onPlaySegment) return
407
+
408
+ setIsManualSyncing(true)
409
+ setSyncWordIndex(0)
410
+ // Start playing 3 seconds before segment start
411
+ const startTime = (editedSegment.start_time ?? 0) - 3
412
+ onPlaySegment(startTime)
413
+ }
414
+
415
+ // Handle play/stop button click
416
+ const handlePlayButtonClick = () => {
417
+ if (!segment?.start_time || !onPlaySegment) return
418
+
419
+ if (isPlaying) {
420
+ // Stop playback
421
+ if (window.toggleAudioPlayback) {
422
+ window.toggleAudioPlayback()
423
+ }
424
+ } else {
425
+ // Start playback
426
+ onPlaySegment(segment.start_time)
427
+ }
428
+ }
429
+
288
430
  return (
289
431
  <Dialog
290
432
  open={open}
291
- onClose={onClose}
433
+ onClose={handleClose}
292
434
  maxWidth="md"
293
435
  fullWidth
294
436
  onKeyDown={handleKeyDown}
@@ -299,10 +441,14 @@ export default function EditModal({
299
441
  {segment?.start_time !== null && onPlaySegment && (
300
442
  <IconButton
301
443
  size="small"
302
- onClick={() => onPlaySegment(segment.start_time!)}
444
+ onClick={handlePlayButtonClick}
303
445
  sx={{ padding: '4px' }}
304
446
  >
305
- <PlayCircleOutlineIcon />
447
+ {isPlaying ? (
448
+ <StopIcon />
449
+ ) : (
450
+ <PlayCircleOutlineIcon />
451
+ )}
306
452
  </IconButton>
307
453
  )}
308
454
  </Box>
@@ -322,11 +468,30 @@ export default function EditModal({
322
468
  />
323
469
  </Box>
324
470
 
325
- <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
326
- Original Time Range: {originalSegment.start_time?.toFixed(2) ?? 'N/A'} - {originalSegment.end_time?.toFixed(2) ?? 'N/A'}
327
- <br />
328
- Current Time Range: {editedSegment.start_time?.toFixed(2) ?? 'N/A'} - {editedSegment.end_time?.toFixed(2) ?? 'N/A'}
329
- </Typography>
471
+ <Box sx={{ mb: 2, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
472
+ <Typography variant="body2" color="text.secondary">
473
+ Original Time Range: {originalSegment.start_time?.toFixed(2) ?? 'N/A'} - {originalSegment.end_time?.toFixed(2) ?? 'N/A'}
474
+ <br />
475
+ Current Time Range: {editedSegment.start_time?.toFixed(2) ?? 'N/A'} - {editedSegment.end_time?.toFixed(2) ?? 'N/A'}
476
+ </Typography>
477
+
478
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
479
+ <Button
480
+ variant={isManualSyncing ? "outlined" : "contained"}
481
+ onClick={startManualSync}
482
+ disabled={!onPlaySegment}
483
+ startIcon={isManualSyncing ? <CancelIcon /> : <PlayCircleOutlineIcon />}
484
+ color={isManualSyncing ? "error" : "primary"}
485
+ >
486
+ {isManualSyncing ? "Cancel Sync" : "Manual Sync"}
487
+ </Button>
488
+ {isManualSyncing && (
489
+ <Typography variant="body2">
490
+ Press spacebar for word {syncWordIndex + 1} of {editedSegment?.words.length}
491
+ </Typography>
492
+ )}
493
+ </Box>
494
+ </Box>
330
495
 
331
496
  <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mb: 3 }}>
332
497
  {editedSegment.words.map((word, index) => (
@@ -413,8 +578,11 @@ export default function EditModal({
413
578
  Delete Segment
414
579
  </Button>
415
580
  </Box>
416
- <Button onClick={onClose}>Cancel</Button>
417
- <Button onClick={handleSave} variant="contained">
581
+ <Button onClick={handleClose}>Cancel</Button>
582
+ <Button onClick={() => {
583
+ cleanupManualSync()
584
+ onSave(editedSegment)
585
+ }}>
418
586
  Save Changes
419
587
  </Button>
420
588
  </DialogActions>
@@ -86,8 +86,6 @@ export default function Header({
86
86
  const addedCount = data.corrections.filter(c => c.split_total).length
87
87
  const deletedCount = data.corrections.filter(c => c.is_deletion).length
88
88
 
89
- console.log('Header: Render with isUpdatingHandlers =', isUpdatingHandlers)
90
-
91
89
  return (
92
90
  <>
93
91
  {isReadOnly && (
@@ -22,7 +22,7 @@ import {
22
22
  updateSegment
23
23
  } from './shared/utils/segmentOperations'
24
24
  import { loadSavedData, saveData, clearSavedData } from './shared/utils/localStorage'
25
- import { setupKeyboardHandlers } from './shared/utils/keyboardHandlers'
25
+ import { setupKeyboardHandlers, setModalHandler } from './shared/utils/keyboardHandlers'
26
26
  import Header from './Header'
27
27
  import { findWordById, getWordsFromIds } from './shared/utils/wordUtils'
28
28
 
@@ -121,19 +121,24 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
121
121
 
122
122
  // Keyboard handlers
123
123
  useEffect(() => {
124
+ console.log('Setting up keyboard handlers in LyricsAnalyzer')
125
+
124
126
  const { handleKeyDown, handleKeyUp } = setupKeyboardHandlers({
125
127
  setIsShiftPressed,
126
128
  setIsCtrlPressed
127
129
  })
128
130
 
131
+ console.log('Adding keyboard event listeners')
129
132
  window.addEventListener('keydown', handleKeyDown)
130
133
  window.addEventListener('keyup', handleKeyUp)
134
+
131
135
  return () => {
136
+ console.log('Removing keyboard event listeners')
132
137
  window.removeEventListener('keydown', handleKeyDown)
133
138
  window.removeEventListener('keyup', handleKeyUp)
134
139
  document.body.style.userSelect = ''
135
140
  }
136
- }, [])
141
+ }, [setIsShiftPressed, setIsCtrlPressed])
137
142
 
138
143
  // Calculate effective mode based on modifier key states
139
144
  const effectiveMode = isShiftPressed ? 'highlight' :
@@ -436,6 +441,12 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
436
441
  }, 1500);
437
442
  }, []);
438
443
 
444
+ // Wrap setModalSpacebarHandler in useCallback
445
+ const handleSetModalSpacebarHandler = useCallback((handler: (() => (e: KeyboardEvent) => void) | undefined) => {
446
+ // Update the global modal handler
447
+ setModalHandler(handler ? handler() : undefined, !!handler)
448
+ }, [])
449
+
439
450
  return (
440
451
  <Box sx={{
441
452
  p: 3,
@@ -505,7 +516,10 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
505
516
 
506
517
  <EditModal
507
518
  open={Boolean(editModalSegment)}
508
- onClose={() => setEditModalSegment(null)}
519
+ onClose={() => {
520
+ setEditModalSegment(null)
521
+ handleSetModalSpacebarHandler(undefined)
522
+ }}
509
523
  segment={editModalSegment?.segment ?? null}
510
524
  segmentIndex={editModalSegment?.index ?? null}
511
525
  originalSegment={editModalSegment?.originalSegment ?? null}
@@ -515,6 +529,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
515
529
  onSplitSegment={handleSplitSegment}
516
530
  onPlaySegment={handlePlaySegment}
517
531
  currentTime={currentAudioTime}
532
+ setModalSpacebarHandler={handleSetModalSpacebarHandler}
518
533
  />
519
534
 
520
535
  <ReviewChangesModal
@@ -524,6 +539,7 @@ export default function LyricsAnalyzer({ data: initialData, onFileLoad, apiClien
524
539
  updatedData={data}
525
540
  onSubmit={handleSubmitToServer}
526
541
  apiClient={apiClient}
542
+ setModalSpacebarHandler={handleSetModalSpacebarHandler}
527
543
  />
528
544
 
529
545
  {!isReadOnly && apiClient && (
@@ -7,12 +7,14 @@ interface PreviewVideoSectionProps {
7
7
  apiClient: ApiClient | null
8
8
  isModalOpen: boolean
9
9
  updatedData: CorrectionData
10
+ videoRef?: React.RefObject<HTMLVideoElement>
10
11
  }
11
12
 
12
13
  export default function PreviewVideoSection({
13
14
  apiClient,
14
15
  isModalOpen,
15
- updatedData
16
+ updatedData,
17
+ videoRef
16
18
  }: PreviewVideoSectionProps) {
17
19
  const [previewState, setPreviewState] = useState<{
18
20
  status: 'loading' | 'ready' | 'error';
@@ -100,6 +102,7 @@ export default function PreviewVideoSection({
100
102
  margin: '0',
101
103
  }}>
102
104
  <video
105
+ ref={videoRef}
103
106
  controls
104
107
  src={previewState.videoUrl}
105
108
  style={{
@@ -1,11 +1,26 @@
1
1
  import { useMemo } from 'react'
2
- import { Paper, Typography, Box } from '@mui/material'
2
+ import { Paper, Typography, Box, IconButton } from '@mui/material'
3
3
  import { ReferenceViewProps } from './shared/types'
4
4
  import { calculateReferenceLinePositions } from './shared/utils/referenceLineCalculator'
5
5
  import { SourceSelector } from './shared/components/SourceSelector'
6
6
  import { HighlightedText } from './shared/components/HighlightedText'
7
7
  import { TranscriptionWordPosition } from './shared/types'
8
8
  import { getWordsFromIds } from './shared/utils/wordUtils'
9
+ import ContentCopyIcon from '@mui/icons-material/ContentCopy'
10
+ import { styled } from '@mui/material/styles'
11
+
12
+ const SegmentControls = styled(Box)({
13
+ display: 'flex',
14
+ alignItems: 'center',
15
+ gap: '4px',
16
+ paddingTop: '3px',
17
+ paddingRight: '8px'
18
+ })
19
+
20
+ const TextContainer = styled(Box)({
21
+ flex: 1,
22
+ minWidth: 0,
23
+ })
9
24
 
10
25
  export default function ReferenceView({
11
26
  referenceSources,
@@ -153,6 +168,11 @@ export default function ReferenceView({
153
168
  // Get the segments for the current source
154
169
  const currentSourceSegments = referenceSources[effectiveCurrentSource]?.segments || [];
155
170
 
171
+ // Helper function to copy text to clipboard
172
+ const copyToClipboard = (text: string) => {
173
+ navigator.clipboard.writeText(text);
174
+ };
175
+
156
176
  return (
157
177
  <Paper sx={{ p: 2 }}>
158
178
  <Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
@@ -166,22 +186,39 @@ export default function ReferenceView({
166
186
  />
167
187
  </Box>
168
188
  <Box sx={{ display: 'flex', flexDirection: 'column' }}>
169
- <HighlightedText
170
- wordPositions={referenceWordPositions}
171
- segments={currentSourceSegments}
172
- anchors={anchors}
173
- onElementClick={onElementClick}
174
- onWordClick={onWordClick}
175
- flashingType={flashingType}
176
- highlightInfo={highlightInfo}
177
- mode={mode}
178
- isReference={true}
179
- currentSource={effectiveCurrentSource}
180
- linePositions={linePositions}
181
- referenceCorrections={referenceCorrections}
182
- gaps={gaps}
183
- preserveSegments={true}
184
- />
189
+ {currentSourceSegments.map((segment, index) => (
190
+ <Box key={index} sx={{ display: 'flex', alignItems: 'flex-start', width: '100%' }}>
191
+ <SegmentControls>
192
+ <IconButton
193
+ size="small"
194
+ onClick={() => copyToClipboard(segment.words.map(w => w.text).join(' '))}
195
+ sx={{ padding: '2px' }}
196
+ >
197
+ <ContentCopyIcon fontSize="small" />
198
+ </IconButton>
199
+ </SegmentControls>
200
+ <TextContainer>
201
+ <HighlightedText
202
+ wordPositions={referenceWordPositions.filter(wp =>
203
+ segment.words.some(w => w.id === wp.word.id)
204
+ )}
205
+ segments={[segment]}
206
+ anchors={anchors}
207
+ onElementClick={onElementClick}
208
+ onWordClick={onWordClick}
209
+ flashingType={flashingType}
210
+ highlightInfo={highlightInfo}
211
+ mode={mode}
212
+ isReference={true}
213
+ currentSource={effectiveCurrentSource}
214
+ linePositions={linePositions}
215
+ referenceCorrections={referenceCorrections}
216
+ gaps={gaps}
217
+ preserveSegments={true}
218
+ />
219
+ </TextContainer>
220
+ </Box>
221
+ ))}
185
222
  </Box>
186
223
  </Paper>
187
224
  )
@@ -9,7 +9,7 @@ import {
9
9
  Paper
10
10
  } from '@mui/material'
11
11
  import { CorrectionData } from '../types'
12
- import { useMemo } from 'react'
12
+ import { useMemo, useRef, useEffect } from 'react'
13
13
  import { ApiClient } from '../api'
14
14
  import PreviewVideoSection from './PreviewVideoSection'
15
15
 
@@ -20,6 +20,7 @@ interface ReviewChangesModalProps {
20
20
  updatedData: CorrectionData
21
21
  onSubmit: () => void
22
22
  apiClient: ApiClient | null
23
+ setModalSpacebarHandler: (handler: (() => (e: KeyboardEvent) => void) | undefined) => void
23
24
  }
24
25
 
25
26
  interface DiffResult {
@@ -66,8 +67,36 @@ export default function ReviewChangesModal({
66
67
  originalData,
67
68
  updatedData,
68
69
  onSubmit,
69
- apiClient
70
+ apiClient,
71
+ setModalSpacebarHandler
70
72
  }: ReviewChangesModalProps) {
73
+ // Add ref to video element
74
+ const videoRef = useRef<HTMLVideoElement>(null)
75
+
76
+ // Add effect to handle spacebar
77
+ useEffect(() => {
78
+ if (open) {
79
+ setModalSpacebarHandler(() => (e: KeyboardEvent) => {
80
+ e.preventDefault()
81
+ e.stopPropagation()
82
+
83
+ if (videoRef.current) {
84
+ if (videoRef.current.paused) {
85
+ videoRef.current.play()
86
+ } else {
87
+ videoRef.current.pause()
88
+ }
89
+ }
90
+ })
91
+ } else {
92
+ setModalSpacebarHandler(undefined)
93
+ }
94
+
95
+ return () => {
96
+ setModalSpacebarHandler(undefined)
97
+ }
98
+ }, [open, setModalSpacebarHandler])
99
+
71
100
  const differences = useMemo(() => {
72
101
  const diffs: DiffResult[] = []
73
102
 
@@ -252,6 +281,7 @@ export default function ReviewChangesModal({
252
281
  apiClient={apiClient}
253
282
  isModalOpen={open}
254
283
  updatedData={updatedData}
284
+ videoRef={videoRef} // Pass the ref to PreviewVideoSection
255
285
  />
256
286
 
257
287
  <Box sx={{ p: 2, mt: 0 }}>
@@ -48,7 +48,6 @@ export default function TranscriptionView({
48
48
  currentTime = 0,
49
49
  anchors = []
50
50
  }: TranscriptionViewProps) {
51
- console.log('TranscriptionView props:', { flashingType, flashingHandler });
52
51
  const [selectedSegmentIndex, setSelectedSegmentIndex] = useState<number | null>(null)
53
52
 
54
53
  return (
@@ -58,8 +58,6 @@ export function HighlightedText({
58
58
  flashingHandler,
59
59
  corrections = [],
60
60
  }: HighlightedTextProps) {
61
- console.log('HighlightedText props:', { flashingType, flashingHandler });
62
-
63
61
  const { handleWordClick } = useWordClick({
64
62
  mode,
65
63
  onElementClick,
@@ -73,7 +71,6 @@ export function HighlightedText({
73
71
 
74
72
  const shouldWordFlash = (wordPos: TranscriptionWordPosition | { word: string; id: string }): boolean => {
75
73
  if (!flashingType) {
76
- console.log('No flashingType');
77
74
  return false;
78
75
  }
79
76