karaoke-gen 0.71.42__py3-none-any.whl → 0.75.16__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 (32) hide show
  1. karaoke_gen/__init__.py +32 -1
  2. karaoke_gen/audio_fetcher.py +476 -56
  3. karaoke_gen/audio_processor.py +11 -3
  4. karaoke_gen/instrumental_review/server.py +154 -860
  5. karaoke_gen/instrumental_review/static/index.html +1506 -0
  6. karaoke_gen/karaoke_finalise/karaoke_finalise.py +62 -1
  7. karaoke_gen/karaoke_gen.py +114 -1
  8. karaoke_gen/lyrics_processor.py +81 -4
  9. karaoke_gen/utils/bulk_cli.py +3 -0
  10. karaoke_gen/utils/cli_args.py +4 -2
  11. karaoke_gen/utils/gen_cli.py +196 -5
  12. karaoke_gen/utils/remote_cli.py +523 -34
  13. {karaoke_gen-0.71.42.dist-info → karaoke_gen-0.75.16.dist-info}/METADATA +4 -1
  14. {karaoke_gen-0.71.42.dist-info → karaoke_gen-0.75.16.dist-info}/RECORD +31 -25
  15. lyrics_transcriber/frontend/package.json +1 -1
  16. lyrics_transcriber/frontend/src/components/Header.tsx +38 -12
  17. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +17 -3
  18. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +185 -0
  19. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +704 -0
  20. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/UpcomingWordsBar.tsx +80 -0
  21. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +905 -0
  22. lyrics_transcriber/frontend/src/components/ModeSelectionModal.tsx +127 -0
  23. lyrics_transcriber/frontend/src/components/ReplaceAllLyricsModal.tsx +190 -542
  24. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
  25. lyrics_transcriber/frontend/web_assets/assets/{index-DdJTDWH3.js → index-COYImAcx.js} +1722 -489
  26. lyrics_transcriber/frontend/web_assets/assets/index-COYImAcx.js.map +1 -0
  27. lyrics_transcriber/frontend/web_assets/index.html +1 -1
  28. lyrics_transcriber/review/server.py +5 -5
  29. lyrics_transcriber/frontend/web_assets/assets/index-DdJTDWH3.js.map +0 -1
  30. {karaoke_gen-0.71.42.dist-info → karaoke_gen-0.75.16.dist-info}/WHEEL +0 -0
  31. {karaoke_gen-0.71.42.dist-info → karaoke_gen-0.75.16.dist-info}/entry_points.txt +0 -0
  32. {karaoke_gen-0.71.42.dist-info → karaoke_gen-0.75.16.dist-info}/licenses/LICENSE +0 -0
@@ -36255,7 +36255,7 @@ const ZoomInIcon = createSvgIcon([/* @__PURE__ */ jsxRuntimeExports.jsx("path",
36255
36255
  const ZoomOutIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
36256
36256
  d: "M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14M7 9h5v1H7z"
36257
36257
  }), "ZoomOut");
36258
- const ArrowBack = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
36258
+ const ArrowBackIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
36259
36259
  d: "M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20z"
36260
36260
  }), "ArrowBack");
36261
36261
  const ArrowForwardIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
@@ -36614,7 +36614,7 @@ const TimelineControls = reactExports.memo(({
36614
36614
  onClick: onScrollLeft,
36615
36615
  disabled: visibleStartTime <= startTime,
36616
36616
  size: "small",
36617
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowBack, {})
36617
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowBackIcon, {})
36618
36618
  }
36619
36619
  ) }),
36620
36620
  /* @__PURE__ */ jsxRuntimeExports.jsx(Tooltip, { title: "Zoom Out (Show More Time)", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -38118,6 +38118,12 @@ function PreviewVideoSection({
38118
38118
  ) })
38119
38119
  ] });
38120
38120
  }
38121
+ const BlockIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
38122
+ d: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2M4 12c0-4.42 3.58-8 8-8 1.85 0 3.55.63 4.9 1.69L5.69 16.9C4.63 15.55 4 13.85 4 12m8 8c-1.85 0-3.55-.63-4.9-1.69L18.31 7.1C19.37 8.45 20 10.15 20 12c0 4.42-3.58 8-8 8"
38123
+ }), "Block");
38124
+ const ClearAllIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
38125
+ d: "M5 13h14v-2H5zm-2 4h14v-2H3zM7 7v2h14V7z"
38126
+ }), "ClearAll");
38121
38127
  const CloudUpload = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
38122
38128
  d: "M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96M14 13v4h-4v-4H7l5-5 5 5z"
38123
38129
  }), "CloudUpload");
@@ -38127,6 +38133,9 @@ const ContentPasteIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("pa
38127
38133
  const EditIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
38128
38134
  d: "M3 17.25V21h3.75L17.81 9.94l-3.75-3.75zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.996.996 0 0 0-1.41 0l-1.83 1.83 3.75 3.75z"
38129
38135
  }), "Edit");
38136
+ const EditNoteIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
38137
+ d: "M3 10h11v2H3zm0-2h11V6H3zm0 8h7v-2H3zm15.01-3.13.71-.71c.39-.39 1.02-.39 1.41 0l.71.71c.39.39.39 1.02 0 1.41l-.71.71zm-.71.71-5.3 5.3V21h2.12l5.3-5.3z"
38138
+ }), "EditNote");
38130
38139
  const FindReplaceIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
38131
38140
  d: "M11 6c1.38 0 2.63.56 3.54 1.46L12 10h6V4l-2.05 2.05C14.68 4.78 12.93 4 11 4c-3.53 0-6.43 2.61-6.92 6H6.1c.46-2.28 2.48-4 4.9-4m5.64 9.14c.66-.9 1.12-1.97 1.28-3.14H15.9c-.46 2.28-2.48 4-4.9 4-1.38 0-2.63-.56-3.54-1.46L10 12H4v6l2.05-2.05C7.32 17.22 9.07 18 11 18c1.55 0 2.98-.51 4.14-1.36L20 21.49 21.49 20z"
38132
38141
  }), "FindReplace");
@@ -38142,12 +38151,18 @@ const OndemandVideo = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path"
38142
38151
  const PauseIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
38143
38152
  d: "M6 19h4V5H6zm8-14v14h4V5z"
38144
38153
  }), "Pause");
38154
+ const RateReviewIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
38155
+ d: "M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2M6 14v-2.47l6.88-6.88c.2-.2.51-.2.71 0l1.77 1.77c.2.2.2.51 0 .71L8.47 14zm12 0h-7.5l2-2H18z"
38156
+ }), "RateReview");
38145
38157
  const RedoIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
38146
38158
  d: "M18.4 10.6C16.55 8.99 14.15 8 11.5 8c-4.65 0-8.58 3.03-9.96 7.22L3.9 16c1.05-3.19 4.05-5.5 7.6-5.5 1.95 0 3.73.72 5.12 1.88L13 16h9V7z"
38147
38159
  }), "Redo");
38148
38160
  const RestoreIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
38149
38161
  d: "M13 3c-4.97 0-9 4.03-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9m-1 5v5l4.28 2.54.72-1.21-3.5-2.08V8z"
38150
38162
  }), "Restore");
38163
+ const SyncIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
38164
+ d: "M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8m0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4z"
38165
+ }), "Sync");
38151
38166
  const TimerIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
38152
38167
  d: "M9 1h6v2H9zm10.03 6.39 1.42-1.42c-.43-.51-.9-.99-1.41-1.41l-1.42 1.42C16.07 4.74 14.12 4 12 4c-4.97 0-9 4.03-9 9s4.02 9 9 9 9-4.03 9-9c0-2.12-.74-4.07-1.97-5.61M13 14h-2V8h2z"
38153
38168
  }), "Timer");
@@ -38408,7 +38423,7 @@ function ReviewChangesModal({
38408
38423
  {
38409
38424
  onClick: onClose,
38410
38425
  color: "warning",
38411
- startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowBack, {}),
38426
+ startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowBackIcon, {}),
38412
38427
  sx: { mr: "auto" },
38413
38428
  children: "Cancel"
38414
38429
  }
@@ -38428,26 +38443,1498 @@ function ReviewChangesModal({
38428
38443
  }
38429
38444
  );
38430
38445
  }
38446
+ function ModeSelectionModal({
38447
+ open,
38448
+ onClose,
38449
+ onSelectReplace,
38450
+ onSelectResync,
38451
+ hasExistingLyrics
38452
+ }) {
38453
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
38454
+ Dialog,
38455
+ {
38456
+ open,
38457
+ onClose,
38458
+ maxWidth: "sm",
38459
+ fullWidth: true,
38460
+ children: [
38461
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(DialogTitle, { sx: { display: "flex", alignItems: "center", gap: 1 }, children: [
38462
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { sx: { flex: 1 }, children: "Edit All Lyrics" }),
38463
+ /* @__PURE__ */ jsxRuntimeExports.jsx(IconButton, { onClick: onClose, sx: { ml: "auto" }, children: /* @__PURE__ */ jsxRuntimeExports.jsx(CloseIcon, {}) })
38464
+ ] }),
38465
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(DialogContent, { dividers: true, children: [
38466
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "body1", sx: { mb: 3 }, children: "Choose how you want to edit the lyrics:" }),
38467
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { display: "flex", flexDirection: "column", gap: 2 }, children: [
38468
+ hasExistingLyrics && /* @__PURE__ */ jsxRuntimeExports.jsx(
38469
+ Paper,
38470
+ {
38471
+ sx: {
38472
+ p: 2,
38473
+ cursor: "pointer",
38474
+ border: 2,
38475
+ borderColor: "primary.main",
38476
+ "&:hover": {
38477
+ bgcolor: "action.hover",
38478
+ borderColor: "primary.dark"
38479
+ }
38480
+ },
38481
+ onClick: onSelectResync,
38482
+ children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { display: "flex", alignItems: "flex-start", gap: 2 }, children: [
38483
+ /* @__PURE__ */ jsxRuntimeExports.jsx(SyncIcon, { color: "primary", sx: { fontSize: 40, mt: 0.5 } }),
38484
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { children: [
38485
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "h6", color: "primary", children: "Re-sync Existing Lyrics" }),
38486
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "body2", color: "text.secondary", children: "Keep the current lyrics text and fix timing issues. Use this when lyrics are correct but timing has drifted, especially in the second half of the song." }),
38487
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "caption", color: "success.main", sx: { mt: 1, display: "block" }, children: "Recommended for fixing timing drift" })
38488
+ ] })
38489
+ ] })
38490
+ }
38491
+ ),
38492
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
38493
+ Paper,
38494
+ {
38495
+ sx: {
38496
+ p: 2,
38497
+ cursor: "pointer",
38498
+ border: 1,
38499
+ borderColor: "divider",
38500
+ "&:hover": {
38501
+ bgcolor: "action.hover",
38502
+ borderColor: "text.secondary"
38503
+ }
38504
+ },
38505
+ onClick: onSelectReplace,
38506
+ children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { display: "flex", alignItems: "flex-start", gap: 2 }, children: [
38507
+ /* @__PURE__ */ jsxRuntimeExports.jsx(ContentPasteIcon, { sx: { fontSize: 40, mt: 0.5, color: "text.secondary" } }),
38508
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { children: [
38509
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "h6", children: "Replace All Lyrics" }),
38510
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "body2", color: "text.secondary", children: "Paste completely new lyrics from clipboard and manually sync timing for all words from scratch." }),
38511
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "caption", color: "warning.main", sx: { mt: 1, display: "block" }, children: "All existing timing data will be lost" })
38512
+ ] })
38513
+ ] })
38514
+ }
38515
+ )
38516
+ ] })
38517
+ ] }),
38518
+ /* @__PURE__ */ jsxRuntimeExports.jsx(DialogActions, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { onClick: onClose, color: "inherit", children: "Cancel" }) })
38519
+ ]
38520
+ }
38521
+ );
38522
+ }
38523
+ const TIME_BAR_HEIGHT = 28;
38524
+ const WORD_BLOCK_HEIGHT = 24;
38525
+ const WORD_LEVEL_SPACING = 50;
38526
+ const CANVAS_PADDING = 8;
38527
+ const TEXT_ABOVE_BLOCK = 14;
38528
+ const RESIZE_HANDLE_SIZE = 8;
38529
+ const RESIZE_HANDLE_HITAREA = 12;
38530
+ const PLAYHEAD_COLOR = "#ffffff";
38531
+ const WORD_BLOCK_COLOR = "#d32f2f";
38532
+ const WORD_BLOCK_SELECTED_COLOR = "#b71c1c";
38533
+ const WORD_BLOCK_CURRENT_COLOR = "#f44336";
38534
+ const WORD_TEXT_CURRENT_COLOR = "#d32f2f";
38535
+ const UPCOMING_WORD_BG = "#fff9c4";
38536
+ const UPCOMING_WORD_TEXT = "#000000";
38537
+ const TIME_BAR_BG = "#f5f5f5";
38538
+ const TIME_BAR_TEXT = "#666666";
38539
+ const TIMELINE_BG = "#e0e0e0";
38540
+ function buildWordToSegmentMap(segments) {
38541
+ const map = /* @__PURE__ */ new Map();
38542
+ segments.forEach((segment, idx) => {
38543
+ segment.words.forEach((word) => {
38544
+ map.set(word.id, idx);
38545
+ });
38546
+ });
38547
+ return map;
38548
+ }
38549
+ function calculateWordLevels(words, segments) {
38550
+ const levels = /* @__PURE__ */ new Map();
38551
+ const wordToSegment = buildWordToSegmentMap(segments);
38552
+ const segmentsWithTiming = segments.map((segment, idx) => {
38553
+ const timedWords = segment.words.filter((w) => w.start_time !== null);
38554
+ const minStart = timedWords.length > 0 ? Math.min(...timedWords.map((w) => w.start_time)) : Infinity;
38555
+ return { idx, minStart };
38556
+ }).filter((s) => s.minStart !== Infinity).sort((a, b) => a.minStart - b.minStart);
38557
+ const segmentLevels = /* @__PURE__ */ new Map();
38558
+ segmentsWithTiming.forEach(({ idx }, orderIndex) => {
38559
+ segmentLevels.set(idx, orderIndex % 2);
38560
+ });
38561
+ for (const word of words) {
38562
+ const segmentIdx = wordToSegment.get(word.id);
38563
+ if (segmentIdx !== void 0 && segmentLevels.has(segmentIdx)) {
38564
+ levels.set(word.id, segmentLevels.get(segmentIdx));
38565
+ } else {
38566
+ levels.set(word.id, 0);
38567
+ }
38568
+ }
38569
+ return levels;
38570
+ }
38571
+ function formatTime(seconds) {
38572
+ const mins = Math.floor(seconds / 60);
38573
+ const secs = Math.floor(seconds % 60);
38574
+ return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
38575
+ }
38576
+ const TimelineCanvas = reactExports.memo(function TimelineCanvas2({
38577
+ words,
38578
+ segments,
38579
+ visibleStartTime,
38580
+ visibleEndTime,
38581
+ currentTime,
38582
+ selectedWordIds,
38583
+ onWordClick,
38584
+ onBackgroundClick,
38585
+ onTimeBarClick,
38586
+ onSelectionComplete,
38587
+ onWordTimingChange,
38588
+ onWordsMove,
38589
+ syncWordIndex,
38590
+ isManualSyncing,
38591
+ onScrollChange,
38592
+ audioDuration,
38593
+ zoomSeconds,
38594
+ height: height2 = 200
38595
+ }) {
38596
+ const canvasRef = reactExports.useRef(null);
38597
+ const containerRef = reactExports.useRef(null);
38598
+ const [canvasWidth, setCanvasWidth] = reactExports.useState(800);
38599
+ const animationFrameRef = reactExports.useRef();
38600
+ const wordLevelsRef = reactExports.useRef(/* @__PURE__ */ new Map());
38601
+ const [dragMode, setDragMode] = reactExports.useState("none");
38602
+ const dragStartRef = reactExports.useRef(null);
38603
+ const dragWordIdRef = reactExports.useRef(null);
38604
+ const dragOriginalTimesRef = reactExports.useRef(/* @__PURE__ */ new Map());
38605
+ const [selectionRect, setSelectionRect] = reactExports.useState(null);
38606
+ const [hoveredWordId, setHoveredWordId] = reactExports.useState(null);
38607
+ const [cursorStyle, setCursorStyle] = reactExports.useState("default");
38608
+ reactExports.useEffect(() => {
38609
+ const updateWidth = () => {
38610
+ if (containerRef.current) {
38611
+ setCanvasWidth(containerRef.current.clientWidth);
38612
+ }
38613
+ };
38614
+ updateWidth();
38615
+ const resizeObserver = new ResizeObserver(updateWidth);
38616
+ if (containerRef.current) {
38617
+ resizeObserver.observe(containerRef.current);
38618
+ }
38619
+ return () => resizeObserver.disconnect();
38620
+ }, []);
38621
+ reactExports.useEffect(() => {
38622
+ wordLevelsRef.current = calculateWordLevels(words, segments);
38623
+ }, [words, segments]);
38624
+ const timeToX = reactExports.useCallback((time) => {
38625
+ const duration2 = visibleEndTime - visibleStartTime;
38626
+ if (duration2 <= 0) return 0;
38627
+ return CANVAS_PADDING + (time - visibleStartTime) / duration2 * (canvasWidth - CANVAS_PADDING * 2);
38628
+ }, [visibleStartTime, visibleEndTime, canvasWidth]);
38629
+ const xToTime = reactExports.useCallback((x) => {
38630
+ const duration2 = visibleEndTime - visibleStartTime;
38631
+ return visibleStartTime + (x - CANVAS_PADDING) / (canvasWidth - CANVAS_PADDING * 2) * duration2;
38632
+ }, [visibleStartTime, visibleEndTime, canvasWidth]);
38633
+ const getWordBounds = reactExports.useCallback((word) => {
38634
+ if (word.start_time === null || word.end_time === null) return null;
38635
+ const level = wordLevelsRef.current.get(word.id) || 0;
38636
+ const startX = timeToX(word.start_time);
38637
+ const endX = timeToX(word.end_time);
38638
+ const blockWidth = Math.max(endX - startX, 4);
38639
+ const y = TIME_BAR_HEIGHT + CANVAS_PADDING + TEXT_ABOVE_BLOCK + level * WORD_LEVEL_SPACING;
38640
+ return { startX, endX, blockWidth, y, level };
38641
+ }, [timeToX]);
38642
+ const isNearResizeHandlePos = reactExports.useCallback((word, x, y) => {
38643
+ const bounds = getWordBounds(word);
38644
+ if (!bounds) return false;
38645
+ const handleX = bounds.startX + bounds.blockWidth - RESIZE_HANDLE_SIZE / 2;
38646
+ const handleY = bounds.y + WORD_BLOCK_HEIGHT / 2;
38647
+ return Math.abs(x - handleX) < RESIZE_HANDLE_HITAREA / 2 && Math.abs(y - handleY) < RESIZE_HANDLE_HITAREA / 2;
38648
+ }, [getWordBounds]);
38649
+ const findWordAtPosition = reactExports.useCallback((x, y) => {
38650
+ for (const word of words) {
38651
+ const bounds = getWordBounds(word);
38652
+ if (!bounds) continue;
38653
+ if (x >= bounds.startX && x <= bounds.startX + bounds.blockWidth && y >= bounds.y && y <= bounds.y + WORD_BLOCK_HEIGHT) {
38654
+ return word;
38655
+ }
38656
+ }
38657
+ return null;
38658
+ }, [words, getWordBounds]);
38659
+ const findWordsInRect = reactExports.useCallback((rect) => {
38660
+ const rectLeft = Math.min(rect.startX, rect.endX);
38661
+ const rectRight = Math.max(rect.startX, rect.endX);
38662
+ const rectTop = Math.min(rect.startY, rect.endY);
38663
+ const rectBottom = Math.max(rect.startY, rect.endY);
38664
+ const selectedIds = [];
38665
+ for (const word of words) {
38666
+ const bounds = getWordBounds(word);
38667
+ if (!bounds) continue;
38668
+ if (bounds.startX + bounds.blockWidth >= rectLeft && bounds.startX <= rectRight && bounds.y + WORD_BLOCK_HEIGHT >= rectTop && bounds.y <= rectBottom) {
38669
+ selectedIds.push(word.id);
38670
+ }
38671
+ }
38672
+ return selectedIds;
38673
+ }, [words, getWordBounds]);
38674
+ const draw = reactExports.useCallback(() => {
38675
+ var _a;
38676
+ const canvas = canvasRef.current;
38677
+ if (!canvas) return;
38678
+ const ctx = canvas.getContext("2d");
38679
+ if (!ctx) return;
38680
+ const dpr = window.devicePixelRatio || 1;
38681
+ canvas.width = canvasWidth * dpr;
38682
+ canvas.height = height2 * dpr;
38683
+ ctx.scale(dpr, dpr);
38684
+ ctx.fillStyle = TIMELINE_BG;
38685
+ ctx.fillRect(0, 0, canvasWidth, height2);
38686
+ ctx.fillStyle = TIME_BAR_BG;
38687
+ ctx.fillRect(0, 0, canvasWidth, TIME_BAR_HEIGHT);
38688
+ const duration2 = visibleEndTime - visibleStartTime;
38689
+ const secondsPerTick = duration2 > 15 ? 2 : duration2 > 8 ? 1 : 0.5;
38690
+ const startSecond = Math.ceil(visibleStartTime / secondsPerTick) * secondsPerTick;
38691
+ ctx.fillStyle = TIME_BAR_TEXT;
38692
+ ctx.font = "11px system-ui, -apple-system, sans-serif";
38693
+ ctx.textAlign = "center";
38694
+ for (let t = startSecond; t <= visibleEndTime; t += secondsPerTick) {
38695
+ const x = timeToX(t);
38696
+ ctx.beginPath();
38697
+ ctx.strokeStyle = "#999999";
38698
+ ctx.lineWidth = 1;
38699
+ ctx.moveTo(x, TIME_BAR_HEIGHT - 6);
38700
+ ctx.lineTo(x, TIME_BAR_HEIGHT);
38701
+ ctx.stroke();
38702
+ if (t % 1 === 0) {
38703
+ ctx.fillText(formatTime(t), x, TIME_BAR_HEIGHT - 10);
38704
+ }
38705
+ }
38706
+ ctx.beginPath();
38707
+ ctx.strokeStyle = "#cccccc";
38708
+ ctx.lineWidth = 1;
38709
+ ctx.moveTo(0, TIME_BAR_HEIGHT);
38710
+ ctx.lineTo(canvasWidth, TIME_BAR_HEIGHT);
38711
+ ctx.stroke();
38712
+ const wordToSegment = buildWordToSegmentMap(segments);
38713
+ const syncedWords = words.filter((w) => w.start_time !== null && w.end_time !== null);
38714
+ const currentWordId = ((_a = syncedWords.find(
38715
+ (w) => currentTime >= w.start_time && currentTime <= w.end_time
38716
+ )) == null ? void 0 : _a.id) || null;
38717
+ for (const word of syncedWords) {
38718
+ const bounds = getWordBounds(word);
38719
+ if (!bounds) continue;
38720
+ const isSelected = selectedWordIds.has(word.id);
38721
+ const isCurrent = word.id === currentWordId;
38722
+ const isHovered = word.id === hoveredWordId;
38723
+ if (isSelected) {
38724
+ ctx.fillStyle = WORD_BLOCK_SELECTED_COLOR;
38725
+ } else if (isCurrent) {
38726
+ ctx.fillStyle = WORD_BLOCK_CURRENT_COLOR;
38727
+ } else {
38728
+ ctx.fillStyle = WORD_BLOCK_COLOR;
38729
+ }
38730
+ ctx.fillRect(bounds.startX, bounds.y, bounds.blockWidth, WORD_BLOCK_HEIGHT);
38731
+ if (isSelected) {
38732
+ ctx.strokeStyle = "#ffffff";
38733
+ ctx.lineWidth = 2;
38734
+ ctx.strokeRect(bounds.startX, bounds.y, bounds.blockWidth, WORD_BLOCK_HEIGHT);
38735
+ if (isHovered || selectedWordIds.size === 1) {
38736
+ const handleX = bounds.startX + bounds.blockWidth - RESIZE_HANDLE_SIZE / 2;
38737
+ const handleY = bounds.y + WORD_BLOCK_HEIGHT / 2;
38738
+ ctx.beginPath();
38739
+ ctx.fillStyle = "#ffffff";
38740
+ ctx.arc(handleX, handleY, RESIZE_HANDLE_SIZE / 2, 0, Math.PI * 2);
38741
+ ctx.fill();
38742
+ ctx.strokeStyle = "#666666";
38743
+ ctx.lineWidth = 1;
38744
+ ctx.stroke();
38745
+ }
38746
+ }
38747
+ }
38748
+ const wordsBySegment = /* @__PURE__ */ new Map();
38749
+ for (const word of syncedWords) {
38750
+ const segIdx = wordToSegment.get(word.id);
38751
+ if (segIdx !== void 0) {
38752
+ if (!wordsBySegment.has(segIdx)) {
38753
+ wordsBySegment.set(segIdx, []);
38754
+ }
38755
+ wordsBySegment.get(segIdx).push(word);
38756
+ }
38757
+ }
38758
+ ctx.font = "11px system-ui, -apple-system, sans-serif";
38759
+ ctx.textAlign = "left";
38760
+ for (const [, segmentWords] of wordsBySegment) {
38761
+ const sortedWords = [...segmentWords].sort(
38762
+ (a, b) => (a.start_time || 0) - (b.start_time || 0)
38763
+ );
38764
+ if (sortedWords.length === 0) continue;
38765
+ const level = wordLevelsRef.current.get(sortedWords[0].id) || 0;
38766
+ const textY = TIME_BAR_HEIGHT + CANVAS_PADDING + TEXT_ABOVE_BLOCK + level * WORD_LEVEL_SPACING - 3;
38767
+ let rightmostTextEnd = -Infinity;
38768
+ for (const word of sortedWords) {
38769
+ const blockStartX = timeToX(word.start_time);
38770
+ const textWidth = ctx.measureText(word.text).width;
38771
+ const textStartX = Math.max(blockStartX, rightmostTextEnd + 3);
38772
+ if (textStartX < canvasWidth - 10) {
38773
+ const isCurrent = word.id === currentWordId;
38774
+ ctx.fillStyle = isCurrent ? WORD_TEXT_CURRENT_COLOR : "#333333";
38775
+ ctx.fillText(word.text, textStartX, textY);
38776
+ rightmostTextEnd = textStartX + textWidth;
38777
+ }
38778
+ }
38779
+ }
38780
+ if (isManualSyncing && syncWordIndex >= 0) {
38781
+ const upcomingWords = words.slice(syncWordIndex).filter((w) => w.start_time === null);
38782
+ const playheadX = timeToX(currentTime);
38783
+ let offsetX = playheadX + 10;
38784
+ ctx.font = "11px system-ui, -apple-system, sans-serif";
38785
+ for (let i = 0; i < Math.min(upcomingWords.length, 12); i++) {
38786
+ const word = upcomingWords[i];
38787
+ const textWidth = ctx.measureText(word.text).width + 10;
38788
+ ctx.fillStyle = UPCOMING_WORD_BG;
38789
+ ctx.fillRect(offsetX, TIME_BAR_HEIGHT + CANVAS_PADDING + WORD_LEVEL_SPACING + 60, textWidth, 20);
38790
+ ctx.fillStyle = UPCOMING_WORD_TEXT;
38791
+ ctx.textAlign = "left";
38792
+ ctx.fillText(word.text, offsetX + 5, TIME_BAR_HEIGHT + CANVAS_PADDING + WORD_LEVEL_SPACING + 74);
38793
+ offsetX += textWidth + 3;
38794
+ if (offsetX > canvasWidth - 20) break;
38795
+ }
38796
+ }
38797
+ if (currentTime >= visibleStartTime && currentTime <= visibleEndTime) {
38798
+ const playheadX = timeToX(currentTime);
38799
+ ctx.beginPath();
38800
+ ctx.fillStyle = PLAYHEAD_COLOR;
38801
+ ctx.strokeStyle = "#333333";
38802
+ ctx.lineWidth = 1;
38803
+ ctx.moveTo(playheadX - 6, 2);
38804
+ ctx.lineTo(playheadX + 6, 2);
38805
+ ctx.lineTo(playheadX, TIME_BAR_HEIGHT - 4);
38806
+ ctx.closePath();
38807
+ ctx.fill();
38808
+ ctx.stroke();
38809
+ ctx.beginPath();
38810
+ ctx.strokeStyle = PLAYHEAD_COLOR;
38811
+ ctx.lineWidth = 2;
38812
+ ctx.moveTo(playheadX, TIME_BAR_HEIGHT);
38813
+ ctx.lineTo(playheadX, height2);
38814
+ ctx.stroke();
38815
+ ctx.beginPath();
38816
+ ctx.strokeStyle = "rgba(0,0,0,0.4)";
38817
+ ctx.lineWidth = 1;
38818
+ ctx.moveTo(playheadX + 1, TIME_BAR_HEIGHT);
38819
+ ctx.lineTo(playheadX + 1, height2);
38820
+ ctx.stroke();
38821
+ }
38822
+ if (selectionRect) {
38823
+ ctx.fillStyle = "rgba(25, 118, 210, 0.2)";
38824
+ ctx.strokeStyle = "rgba(25, 118, 210, 0.8)";
38825
+ ctx.lineWidth = 1;
38826
+ const rectX = Math.min(selectionRect.startX, selectionRect.endX);
38827
+ const rectY = Math.min(selectionRect.startY, selectionRect.endY);
38828
+ const rectW = Math.abs(selectionRect.endX - selectionRect.startX);
38829
+ const rectH = Math.abs(selectionRect.endY - selectionRect.startY);
38830
+ ctx.fillRect(rectX, rectY, rectW, rectH);
38831
+ ctx.strokeRect(rectX, rectY, rectW, rectH);
38832
+ }
38833
+ }, [
38834
+ canvasWidth,
38835
+ height2,
38836
+ visibleStartTime,
38837
+ visibleEndTime,
38838
+ currentTime,
38839
+ words,
38840
+ segments,
38841
+ selectedWordIds,
38842
+ selectionRect,
38843
+ hoveredWordId,
38844
+ syncWordIndex,
38845
+ isManualSyncing,
38846
+ timeToX,
38847
+ getWordBounds
38848
+ ]);
38849
+ reactExports.useEffect(() => {
38850
+ const animate = () => {
38851
+ draw();
38852
+ animationFrameRef.current = requestAnimationFrame(animate);
38853
+ };
38854
+ animate();
38855
+ return () => {
38856
+ if (animationFrameRef.current) {
38857
+ cancelAnimationFrame(animationFrameRef.current);
38858
+ }
38859
+ };
38860
+ }, [draw]);
38861
+ const handleMouseDown = reactExports.useCallback((e) => {
38862
+ var _a;
38863
+ const rect = (_a = canvasRef.current) == null ? void 0 : _a.getBoundingClientRect();
38864
+ if (!rect) return;
38865
+ const x = e.clientX - rect.left;
38866
+ const y = e.clientY - rect.top;
38867
+ const time = xToTime(x);
38868
+ if (y < TIME_BAR_HEIGHT) {
38869
+ onTimeBarClick(Math.max(0, time));
38870
+ return;
38871
+ }
38872
+ const clickedWord = findWordAtPosition(x, y);
38873
+ if (clickedWord && selectedWordIds.has(clickedWord.id)) {
38874
+ if (isNearResizeHandlePos(clickedWord, x, y)) {
38875
+ setDragMode("resize");
38876
+ dragStartRef.current = { x, y, time };
38877
+ dragWordIdRef.current = clickedWord.id;
38878
+ dragOriginalTimesRef.current = /* @__PURE__ */ new Map([[clickedWord.id, {
38879
+ start: clickedWord.start_time,
38880
+ end: clickedWord.end_time
38881
+ }]]);
38882
+ return;
38883
+ }
38884
+ setDragMode("move");
38885
+ dragStartRef.current = { x, y, time };
38886
+ dragWordIdRef.current = clickedWord.id;
38887
+ const originalTimes = /* @__PURE__ */ new Map();
38888
+ for (const wordId of selectedWordIds) {
38889
+ const word = words.find((w) => w.id === wordId);
38890
+ if (word && word.start_time !== null && word.end_time !== null) {
38891
+ originalTimes.set(wordId, { start: word.start_time, end: word.end_time });
38892
+ }
38893
+ }
38894
+ dragOriginalTimesRef.current = originalTimes;
38895
+ return;
38896
+ }
38897
+ if (clickedWord) {
38898
+ onWordClick(clickedWord.id, e);
38899
+ return;
38900
+ }
38901
+ setDragMode("selection");
38902
+ dragStartRef.current = { x, y, time };
38903
+ setSelectionRect({ startX: x, startY: y, endX: x, endY: y });
38904
+ }, [xToTime, onTimeBarClick, findWordAtPosition, selectedWordIds, isNearResizeHandlePos, onWordClick, words]);
38905
+ const handleMouseMove = reactExports.useCallback((e) => {
38906
+ var _a;
38907
+ const rect = (_a = canvasRef.current) == null ? void 0 : _a.getBoundingClientRect();
38908
+ if (!rect) return;
38909
+ const x = e.clientX - rect.left;
38910
+ const y = e.clientY - rect.top;
38911
+ const time = xToTime(x);
38912
+ if (dragMode === "none") {
38913
+ const hoveredWord = findWordAtPosition(x, y);
38914
+ setHoveredWordId((hoveredWord == null ? void 0 : hoveredWord.id) || null);
38915
+ if (hoveredWord && selectedWordIds.has(hoveredWord.id)) {
38916
+ const nearHandle = isNearResizeHandlePos(hoveredWord, x, y);
38917
+ setCursorStyle(nearHandle ? "ew-resize" : "grab");
38918
+ } else if (hoveredWord) {
38919
+ setCursorStyle("pointer");
38920
+ } else if (y < TIME_BAR_HEIGHT) {
38921
+ setCursorStyle("pointer");
38922
+ } else {
38923
+ setCursorStyle("default");
38924
+ }
38925
+ }
38926
+ if (!dragStartRef.current) return;
38927
+ if (dragMode === "selection") {
38928
+ setSelectionRect({
38929
+ startX: dragStartRef.current.x,
38930
+ startY: dragStartRef.current.y,
38931
+ endX: x,
38932
+ endY: y
38933
+ });
38934
+ } else if (dragMode === "resize" && dragWordIdRef.current) {
38935
+ const originalTimes = dragOriginalTimesRef.current.get(dragWordIdRef.current);
38936
+ if (originalTimes) {
38937
+ const deltaTime = time - dragStartRef.current.time;
38938
+ const newEndTime = Math.max(originalTimes.start + 0.05, originalTimes.end + deltaTime);
38939
+ onWordTimingChange(dragWordIdRef.current, originalTimes.start, newEndTime);
38940
+ }
38941
+ setCursorStyle("ew-resize");
38942
+ } else if (dragMode === "move") {
38943
+ const deltaTime = time - dragStartRef.current.time;
38944
+ const updates = [];
38945
+ for (const [wordId, originalTimes] of dragOriginalTimesRef.current) {
38946
+ updates.push({
38947
+ wordId,
38948
+ newStartTime: Math.max(0, originalTimes.start + deltaTime),
38949
+ newEndTime: Math.max(0.05, originalTimes.end + deltaTime)
38950
+ });
38951
+ }
38952
+ if (updates.length > 0) {
38953
+ onWordsMove(updates);
38954
+ }
38955
+ setCursorStyle("grabbing");
38956
+ }
38957
+ }, [dragMode, xToTime, findWordAtPosition, selectedWordIds, isNearResizeHandlePos, onWordTimingChange, onWordsMove]);
38958
+ const handleMouseUp = reactExports.useCallback((e) => {
38959
+ var _a;
38960
+ const rect = (_a = canvasRef.current) == null ? void 0 : _a.getBoundingClientRect();
38961
+ if (dragMode === "selection" && dragStartRef.current && rect) {
38962
+ const endX = e.clientX - rect.left;
38963
+ const endY = e.clientY - rect.top;
38964
+ const dragDistance = Math.sqrt(
38965
+ Math.pow(endX - dragStartRef.current.x, 2) + Math.pow(endY - dragStartRef.current.y, 2)
38966
+ );
38967
+ if (dragDistance < 5) {
38968
+ onBackgroundClick();
38969
+ } else {
38970
+ const finalRect = {
38971
+ startX: dragStartRef.current.x,
38972
+ startY: dragStartRef.current.y,
38973
+ endX,
38974
+ endY
38975
+ };
38976
+ const selectedIds = findWordsInRect(finalRect);
38977
+ if (selectedIds.length > 0) {
38978
+ onSelectionComplete(selectedIds);
38979
+ }
38980
+ }
38981
+ }
38982
+ setDragMode("none");
38983
+ dragStartRef.current = null;
38984
+ dragWordIdRef.current = null;
38985
+ dragOriginalTimesRef.current = /* @__PURE__ */ new Map();
38986
+ setSelectionRect(null);
38987
+ setCursorStyle("default");
38988
+ }, [dragMode, onBackgroundClick, findWordsInRect, onSelectionComplete]);
38989
+ const handleWheel = reactExports.useCallback((e) => {
38990
+ const delta = e.deltaX !== 0 ? e.deltaX : e.deltaY;
38991
+ const scrollAmount = delta / 100 * (zoomSeconds / 4);
38992
+ let newStart = Math.max(0, Math.min(audioDuration - zoomSeconds, visibleStartTime + scrollAmount));
38993
+ if (newStart !== visibleStartTime) {
38994
+ onScrollChange(newStart);
38995
+ }
38996
+ }, [visibleStartTime, zoomSeconds, audioDuration, onScrollChange]);
38997
+ const handleScrollLeft = reactExports.useCallback(() => {
38998
+ const newStart = Math.max(0, visibleStartTime - zoomSeconds * 0.25);
38999
+ onScrollChange(newStart);
39000
+ }, [visibleStartTime, zoomSeconds, onScrollChange]);
39001
+ const handleScrollRight = reactExports.useCallback(() => {
39002
+ const newStart = Math.min(audioDuration - zoomSeconds, visibleStartTime + zoomSeconds * 0.25);
39003
+ onScrollChange(Math.max(0, newStart));
39004
+ }, [visibleStartTime, zoomSeconds, audioDuration, onScrollChange]);
39005
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { sx: { display: "flex", flexDirection: "column", gap: 0.5 }, children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: 1 }, children: [
39006
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Tooltip, { title: "Scroll Left", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
39007
+ IconButton,
39008
+ {
39009
+ size: "small",
39010
+ onClick: handleScrollLeft,
39011
+ disabled: visibleStartTime <= 0,
39012
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowBackIcon, { fontSize: "small" })
39013
+ }
39014
+ ) }),
39015
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
39016
+ Box,
39017
+ {
39018
+ ref: containerRef,
39019
+ sx: {
39020
+ flexGrow: 1,
39021
+ height: height2,
39022
+ cursor: cursorStyle,
39023
+ borderRadius: 1,
39024
+ overflow: "hidden"
39025
+ },
39026
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(
39027
+ "canvas",
39028
+ {
39029
+ ref: canvasRef,
39030
+ style: {
39031
+ width: "100%",
39032
+ height: "100%",
39033
+ display: "block",
39034
+ cursor: cursorStyle
39035
+ },
39036
+ onMouseDown: handleMouseDown,
39037
+ onMouseMove: handleMouseMove,
39038
+ onMouseUp: handleMouseUp,
39039
+ onMouseLeave: handleMouseUp,
39040
+ onWheel: handleWheel
39041
+ }
39042
+ )
39043
+ }
39044
+ ),
39045
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Tooltip, { title: "Scroll Right", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
39046
+ IconButton,
39047
+ {
39048
+ size: "small",
39049
+ onClick: handleScrollRight,
39050
+ disabled: visibleStartTime >= audioDuration - zoomSeconds,
39051
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowForwardIcon, { fontSize: "small" })
39052
+ }
39053
+ ) })
39054
+ ] }) });
39055
+ });
39056
+ const UpcomingWordsBar = reactExports.memo(function UpcomingWordsBar2({
39057
+ words,
39058
+ syncWordIndex,
39059
+ isManualSyncing,
39060
+ maxWordsToShow = 20
39061
+ }) {
39062
+ const upcomingWords = reactExports.useMemo(() => {
39063
+ if (!isManualSyncing || syncWordIndex < 0) return [];
39064
+ return words.slice(syncWordIndex).filter((w) => w.start_time === null).slice(0, maxWordsToShow);
39065
+ }, [words, syncWordIndex, isManualSyncing, maxWordsToShow]);
39066
+ const totalRemaining = reactExports.useMemo(() => {
39067
+ if (!isManualSyncing || syncWordIndex < 0) return 0;
39068
+ return words.slice(syncWordIndex).filter((w) => w.start_time === null).length;
39069
+ }, [words, syncWordIndex, isManualSyncing]);
39070
+ if (upcomingWords.length === 0) {
39071
+ return null;
39072
+ }
39073
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: {
39074
+ height: 44,
39075
+ bgcolor: "grey.100",
39076
+ borderRadius: 1,
39077
+ display: "flex",
39078
+ alignItems: "center",
39079
+ px: 1,
39080
+ gap: 0.5,
39081
+ overflow: "hidden",
39082
+ boxSizing: "border-box"
39083
+ }, children: [
39084
+ upcomingWords.map((word, index) => /* @__PURE__ */ jsxRuntimeExports.jsx(
39085
+ Box,
39086
+ {
39087
+ sx: {
39088
+ px: 1,
39089
+ py: 0.5,
39090
+ borderRadius: 0.5,
39091
+ bgcolor: index === 0 ? "error.main" : "grey.300",
39092
+ color: index === 0 ? "white" : "text.primary",
39093
+ fontWeight: index === 0 ? "bold" : "normal",
39094
+ fontSize: "13px",
39095
+ fontFamily: "system-ui, -apple-system, sans-serif",
39096
+ whiteSpace: "nowrap",
39097
+ border: index === 0 ? "2px solid" : "none",
39098
+ borderColor: "error.dark"
39099
+ },
39100
+ children: word.text
39101
+ },
39102
+ word.id
39103
+ )),
39104
+ totalRemaining > maxWordsToShow && /* @__PURE__ */ jsxRuntimeExports.jsxs(Typography, { variant: "caption", color: "text.secondary", sx: { ml: 1 }, children: [
39105
+ "+",
39106
+ totalRemaining - maxWordsToShow,
39107
+ " more"
39108
+ ] })
39109
+ ] });
39110
+ });
39111
+ const SyncControls = reactExports.memo(function SyncControls2({
39112
+ isManualSyncing,
39113
+ isPaused,
39114
+ onStartSync,
39115
+ onPauseSync,
39116
+ onResumeSync,
39117
+ onClearSync,
39118
+ onEditLyrics,
39119
+ onPlay,
39120
+ onStop,
39121
+ isPlaying,
39122
+ hasSelectedWords,
39123
+ selectedWordCount,
39124
+ onUnsyncFromCursor,
39125
+ onEditSelectedWord,
39126
+ onDeleteSelected,
39127
+ canUnsyncFromCursor
39128
+ }) {
39129
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { display: "flex", flexDirection: "column", gap: 1.5 }, children: [
39130
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", flexWrap: "wrap", children: [
39131
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
39132
+ Button,
39133
+ {
39134
+ variant: "outlined",
39135
+ color: "primary",
39136
+ onClick: onPlay,
39137
+ startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(PlayArrowIcon, {}),
39138
+ size: "small",
39139
+ disabled: isPlaying,
39140
+ children: "Play"
39141
+ }
39142
+ ),
39143
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
39144
+ Button,
39145
+ {
39146
+ variant: "outlined",
39147
+ color: "error",
39148
+ onClick: onStop,
39149
+ startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(StopIcon, {}),
39150
+ size: "small",
39151
+ disabled: !isPlaying,
39152
+ children: "Stop"
39153
+ }
39154
+ ),
39155
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Divider, { orientation: "vertical", flexItem: true, sx: { mx: 0.5 } }),
39156
+ isManualSyncing ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
39157
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
39158
+ Button,
39159
+ {
39160
+ variant: "contained",
39161
+ color: "error",
39162
+ onClick: onStartSync,
39163
+ startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(StopIcon, {}),
39164
+ size: "small",
39165
+ children: "Stop Sync"
39166
+ }
39167
+ ),
39168
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
39169
+ Button,
39170
+ {
39171
+ variant: "outlined",
39172
+ color: isPaused ? "success" : "warning",
39173
+ onClick: isPaused ? onResumeSync : onPauseSync,
39174
+ startIcon: isPaused ? /* @__PURE__ */ jsxRuntimeExports.jsx(PlayArrowIcon, {}) : /* @__PURE__ */ jsxRuntimeExports.jsx(PauseIcon, {}),
39175
+ size: "small",
39176
+ children: isPaused ? "Resume" : "Pause"
39177
+ }
39178
+ )
39179
+ ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx(
39180
+ Button,
39181
+ {
39182
+ variant: "contained",
39183
+ color: "primary",
39184
+ onClick: onStartSync,
39185
+ startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(PlayCircleOutlineIcon, {}),
39186
+ size: "small",
39187
+ children: "Start Sync"
39188
+ }
39189
+ )
39190
+ ] }),
39191
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", flexWrap: "wrap", children: [
39192
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
39193
+ Button,
39194
+ {
39195
+ variant: "outlined",
39196
+ color: "warning",
39197
+ onClick: onClearSync,
39198
+ startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(ClearAllIcon, {}),
39199
+ size: "small",
39200
+ disabled: isManualSyncing && !isPaused,
39201
+ children: "Clear Sync"
39202
+ }
39203
+ ),
39204
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
39205
+ Button,
39206
+ {
39207
+ variant: "outlined",
39208
+ onClick: onEditLyrics,
39209
+ startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(EditNoteIcon, {}),
39210
+ size: "small",
39211
+ disabled: isManualSyncing && !isPaused,
39212
+ children: "Edit Lyrics"
39213
+ }
39214
+ ),
39215
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Divider, { orientation: "vertical", flexItem: true, sx: { mx: 0.5 } }),
39216
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
39217
+ Button,
39218
+ {
39219
+ variant: "outlined",
39220
+ onClick: onUnsyncFromCursor,
39221
+ startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(BlockIcon, {}),
39222
+ size: "small",
39223
+ disabled: !canUnsyncFromCursor || isManualSyncing && !isPaused,
39224
+ children: "Unsync from Cursor"
39225
+ }
39226
+ ),
39227
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
39228
+ Button,
39229
+ {
39230
+ variant: "outlined",
39231
+ onClick: onEditSelectedWord,
39232
+ startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(EditIcon, {}),
39233
+ size: "small",
39234
+ disabled: !hasSelectedWords || selectedWordCount !== 1,
39235
+ children: "Edit Word"
39236
+ }
39237
+ ),
39238
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
39239
+ Button,
39240
+ {
39241
+ variant: "outlined",
39242
+ color: "error",
39243
+ onClick: onDeleteSelected,
39244
+ startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(DeleteIcon, {}),
39245
+ size: "small",
39246
+ disabled: !hasSelectedWords || isManualSyncing && !isPaused,
39247
+ children: [
39248
+ "Delete",
39249
+ hasSelectedWords && selectedWordCount > 0 ? ` (${selectedWordCount})` : ""
39250
+ ]
39251
+ }
39252
+ )
39253
+ ] })
39254
+ ] });
39255
+ });
39256
+ const MIN_ZOOM_SECONDS = 4.5;
39257
+ const MAX_ZOOM_SECONDS = 24;
39258
+ const ZOOM_STEPS = 50;
39259
+ function getAllWords(segments) {
39260
+ return segments.flatMap((s) => s.words);
39261
+ }
39262
+ function cloneSegments(segments) {
39263
+ return JSON.parse(JSON.stringify(segments));
39264
+ }
39265
+ const LyricsSynchronizer = reactExports.memo(function LyricsSynchronizer2({
39266
+ segments: initialSegments,
39267
+ currentTime,
39268
+ onPlaySegment,
39269
+ onSave,
39270
+ onCancel,
39271
+ setModalSpacebarHandler
39272
+ }) {
39273
+ const [workingSegments, setWorkingSegments] = reactExports.useState(
39274
+ () => cloneSegments(initialSegments)
39275
+ );
39276
+ const allWords = reactExports.useMemo(() => getAllWords(workingSegments), [workingSegments]);
39277
+ const audioDuration = reactExports.useMemo(() => {
39278
+ if (window.getAudioDuration) {
39279
+ const duration2 = window.getAudioDuration();
39280
+ return duration2 > 0 ? duration2 : 300;
39281
+ }
39282
+ return 300;
39283
+ }, []);
39284
+ const [zoomSeconds, setZoomSeconds] = reactExports.useState(12);
39285
+ const [visibleStartTime, setVisibleStartTime] = reactExports.useState(0);
39286
+ const visibleEndTime = reactExports.useMemo(
39287
+ () => Math.min(visibleStartTime + zoomSeconds, audioDuration),
39288
+ [visibleStartTime, zoomSeconds, audioDuration]
39289
+ );
39290
+ const [isManualSyncing, setIsManualSyncing] = reactExports.useState(false);
39291
+ const [isPaused, setIsPaused] = reactExports.useState(false);
39292
+ const [syncWordIndex, setSyncWordIndex] = reactExports.useState(-1);
39293
+ const [isSpacebarPressed, setIsSpacebarPressed] = reactExports.useState(false);
39294
+ const wordStartTimeRef = reactExports.useRef(null);
39295
+ const spacebarPressTimeRef = reactExports.useRef(null);
39296
+ const currentTimeRef = reactExports.useRef(currentTime);
39297
+ const [selectedWordIds, setSelectedWordIds] = reactExports.useState(/* @__PURE__ */ new Set());
39298
+ const [showEditLyricsModal, setShowEditLyricsModal] = reactExports.useState(false);
39299
+ const [editLyricsText, setEditLyricsText] = reactExports.useState("");
39300
+ const [showEditWordModal, setShowEditWordModal] = reactExports.useState(false);
39301
+ const [editWordText, setEditWordText] = reactExports.useState("");
39302
+ const [editWordId, setEditWordId] = reactExports.useState(null);
39303
+ reactExports.useEffect(() => {
39304
+ currentTimeRef.current = currentTime;
39305
+ }, [currentTime]);
39306
+ reactExports.useEffect(() => {
39307
+ if (isManualSyncing && !isPaused && currentTime > 0) {
39308
+ if (currentTime > visibleEndTime - zoomSeconds * 0.1) {
39309
+ const newStart = Math.max(0, currentTime - zoomSeconds * 0.1);
39310
+ setVisibleStartTime(newStart);
39311
+ } else if (currentTime < visibleStartTime) {
39312
+ setVisibleStartTime(Math.max(0, currentTime - 1));
39313
+ }
39314
+ }
39315
+ }, [currentTime, isManualSyncing, isPaused, visibleStartTime, visibleEndTime, zoomSeconds]);
39316
+ const handleZoomChange = reactExports.useCallback((_, value) => {
39317
+ const zoomValue = value;
39318
+ const newZoomSeconds = MIN_ZOOM_SECONDS + zoomValue / ZOOM_STEPS * (MAX_ZOOM_SECONDS - MIN_ZOOM_SECONDS);
39319
+ setZoomSeconds(newZoomSeconds);
39320
+ }, []);
39321
+ const sliderValue = reactExports.useMemo(() => {
39322
+ return (zoomSeconds - MIN_ZOOM_SECONDS) / (MAX_ZOOM_SECONDS - MIN_ZOOM_SECONDS) * ZOOM_STEPS;
39323
+ }, [zoomSeconds]);
39324
+ const handleScrollChange = reactExports.useCallback((newStartTime) => {
39325
+ setVisibleStartTime(newStartTime);
39326
+ }, []);
39327
+ const updateWords = reactExports.useCallback((newWords) => {
39328
+ setWorkingSegments((prevSegments) => {
39329
+ const newSegments = cloneSegments(prevSegments);
39330
+ const wordMap = new Map(newWords.map((w) => [w.id, w]));
39331
+ for (const segment of newSegments) {
39332
+ segment.words = segment.words.map((w) => wordMap.get(w.id) || w);
39333
+ const timedWords = segment.words.filter(
39334
+ (w) => w.start_time !== null && w.end_time !== null
39335
+ );
39336
+ if (timedWords.length > 0) {
39337
+ segment.start_time = Math.min(...timedWords.map((w) => w.start_time));
39338
+ segment.end_time = Math.max(...timedWords.map((w) => w.end_time));
39339
+ } else {
39340
+ segment.start_time = null;
39341
+ segment.end_time = null;
39342
+ }
39343
+ }
39344
+ return newSegments;
39345
+ });
39346
+ }, []);
39347
+ const [isPlaying, setIsPlaying] = reactExports.useState(false);
39348
+ reactExports.useEffect(() => {
39349
+ const checkPlaying = () => {
39350
+ setIsPlaying(window.isAudioPlaying || false);
39351
+ };
39352
+ checkPlaying();
39353
+ const interval = setInterval(checkPlaying, 100);
39354
+ return () => clearInterval(interval);
39355
+ }, []);
39356
+ const handlePlayAudio = reactExports.useCallback(() => {
39357
+ if (onPlaySegment) {
39358
+ onPlaySegment(currentTimeRef.current);
39359
+ }
39360
+ }, [onPlaySegment]);
39361
+ const handleStopAudio = reactExports.useCallback(() => {
39362
+ if (window.toggleAudioPlayback && window.isAudioPlaying) {
39363
+ window.toggleAudioPlayback();
39364
+ }
39365
+ if (isManualSyncing) {
39366
+ setIsManualSyncing(false);
39367
+ setIsPaused(false);
39368
+ setIsSpacebarPressed(false);
39369
+ }
39370
+ }, [isManualSyncing]);
39371
+ const handleStartSync = reactExports.useCallback(() => {
39372
+ if (isManualSyncing) {
39373
+ setIsManualSyncing(false);
39374
+ setIsPaused(false);
39375
+ setSyncWordIndex(-1);
39376
+ setIsSpacebarPressed(false);
39377
+ handleStopAudio();
39378
+ return;
39379
+ }
39380
+ const firstUnsyncedIndex = allWords.findIndex(
39381
+ (w) => w.start_time === null || w.end_time === null
39382
+ );
39383
+ const startIndex = firstUnsyncedIndex !== -1 ? firstUnsyncedIndex : 0;
39384
+ setIsManualSyncing(true);
39385
+ setIsPaused(false);
39386
+ setSyncWordIndex(startIndex);
39387
+ setIsSpacebarPressed(false);
39388
+ if (onPlaySegment) {
39389
+ onPlaySegment(Math.max(0, currentTimeRef.current - 1));
39390
+ }
39391
+ }, [isManualSyncing, allWords, onPlaySegment, handleStopAudio]);
39392
+ const handlePauseSync = reactExports.useCallback(() => {
39393
+ setIsPaused(true);
39394
+ handleStopAudio();
39395
+ }, [handleStopAudio]);
39396
+ const handleResumeSync = reactExports.useCallback(() => {
39397
+ setIsPaused(false);
39398
+ const firstUnsyncedIndex = allWords.findIndex(
39399
+ (w) => w.start_time === null || w.end_time === null
39400
+ );
39401
+ if (firstUnsyncedIndex !== -1 && firstUnsyncedIndex !== syncWordIndex) {
39402
+ setSyncWordIndex(firstUnsyncedIndex);
39403
+ }
39404
+ if (onPlaySegment) {
39405
+ onPlaySegment(currentTimeRef.current);
39406
+ }
39407
+ }, [allWords, syncWordIndex, onPlaySegment]);
39408
+ const handleClearSync = reactExports.useCallback(() => {
39409
+ setWorkingSegments((prevSegments) => {
39410
+ const newSegments = cloneSegments(prevSegments);
39411
+ for (const segment of newSegments) {
39412
+ for (const word of segment.words) {
39413
+ word.start_time = null;
39414
+ word.end_time = null;
39415
+ }
39416
+ segment.start_time = null;
39417
+ segment.end_time = null;
39418
+ }
39419
+ return newSegments;
39420
+ });
39421
+ setSyncWordIndex(-1);
39422
+ }, []);
39423
+ const handleUnsyncFromCursor = reactExports.useCallback(() => {
39424
+ const cursorTime = currentTimeRef.current;
39425
+ setWorkingSegments((prevSegments) => {
39426
+ const newSegments = cloneSegments(prevSegments);
39427
+ for (const segment of newSegments) {
39428
+ for (const word of segment.words) {
39429
+ if (word.start_time !== null && word.start_time > cursorTime) {
39430
+ word.start_time = null;
39431
+ word.end_time = null;
39432
+ }
39433
+ }
39434
+ const timedWords = segment.words.filter(
39435
+ (w) => w.start_time !== null && w.end_time !== null
39436
+ );
39437
+ if (timedWords.length > 0) {
39438
+ segment.start_time = Math.min(...timedWords.map((w) => w.start_time));
39439
+ segment.end_time = Math.max(...timedWords.map((w) => w.end_time));
39440
+ } else {
39441
+ segment.start_time = null;
39442
+ segment.end_time = null;
39443
+ }
39444
+ }
39445
+ return newSegments;
39446
+ });
39447
+ }, []);
39448
+ const canUnsyncFromCursor = reactExports.useMemo(() => {
39449
+ const cursorTime = currentTimeRef.current;
39450
+ return allWords.some(
39451
+ (w) => w.start_time !== null && w.start_time > cursorTime
39452
+ );
39453
+ }, [allWords, currentTime]);
39454
+ const handleEditLyrics = reactExports.useCallback(() => {
39455
+ const text = workingSegments.map((s) => s.text).join("\n");
39456
+ setEditLyricsText(text);
39457
+ setShowEditLyricsModal(true);
39458
+ }, [workingSegments]);
39459
+ const handleSaveEditedLyrics = reactExports.useCallback(() => {
39460
+ const lines = editLyricsText.split("\n").filter((l) => l.trim());
39461
+ const newSegments = lines.map((line2, idx) => {
39462
+ const words = line2.trim().split(/\s+/).map((text, wIdx) => ({
39463
+ id: `word-${idx}-${wIdx}-${Date.now()}`,
39464
+ text,
39465
+ start_time: null,
39466
+ end_time: null,
39467
+ confidence: 1
39468
+ }));
39469
+ return {
39470
+ id: `segment-${idx}-${Date.now()}`,
39471
+ text: line2.trim(),
39472
+ words,
39473
+ start_time: null,
39474
+ end_time: null
39475
+ };
39476
+ });
39477
+ setWorkingSegments(newSegments);
39478
+ setShowEditLyricsModal(false);
39479
+ setSyncWordIndex(-1);
39480
+ }, [editLyricsText]);
39481
+ const handleEditSelectedWord = reactExports.useCallback(() => {
39482
+ if (selectedWordIds.size !== 1) return;
39483
+ const wordId = Array.from(selectedWordIds)[0];
39484
+ const word = allWords.find((w) => w.id === wordId);
39485
+ if (word) {
39486
+ setEditWordId(wordId);
39487
+ setEditWordText(word.text);
39488
+ setShowEditWordModal(true);
39489
+ }
39490
+ }, [selectedWordIds, allWords]);
39491
+ const handleSaveEditedWord = reactExports.useCallback(() => {
39492
+ if (!editWordId) return;
39493
+ const newText = editWordText.trim();
39494
+ if (!newText) return;
39495
+ const newWords = newText.split(/\s+/);
39496
+ if (newWords.length === 1) {
39497
+ const updatedWords = allWords.map(
39498
+ (w) => w.id === editWordId ? { ...w, text: newWords[0] } : w
39499
+ );
39500
+ updateWords(updatedWords);
39501
+ } else {
39502
+ const originalWord = allWords.find((w) => w.id === editWordId);
39503
+ if (!originalWord) return;
39504
+ setWorkingSegments((prevSegments) => {
39505
+ const newSegments = cloneSegments(prevSegments);
39506
+ for (const segment of newSegments) {
39507
+ const wordIndex = segment.words.findIndex((w) => w.id === editWordId);
39508
+ if (wordIndex !== -1) {
39509
+ const newWordObjects = newWords.map((text, idx) => ({
39510
+ id: idx === 0 ? editWordId : `${editWordId}-split-${idx}`,
39511
+ text,
39512
+ start_time: idx === 0 ? originalWord.start_time : null,
39513
+ end_time: idx === 0 ? originalWord.end_time : null,
39514
+ confidence: 1
39515
+ }));
39516
+ segment.words.splice(wordIndex, 1, ...newWordObjects);
39517
+ segment.text = segment.words.map((w) => w.text).join(" ");
39518
+ break;
39519
+ }
39520
+ }
39521
+ return newSegments;
39522
+ });
39523
+ }
39524
+ setShowEditWordModal(false);
39525
+ setEditWordId(null);
39526
+ setEditWordText("");
39527
+ setSelectedWordIds(/* @__PURE__ */ new Set());
39528
+ }, [editWordId, editWordText, allWords, updateWords]);
39529
+ const handleDeleteSelected = reactExports.useCallback(() => {
39530
+ if (selectedWordIds.size === 0) return;
39531
+ setWorkingSegments((prevSegments) => {
39532
+ const newSegments = cloneSegments(prevSegments);
39533
+ for (const segment of newSegments) {
39534
+ segment.words = segment.words.filter((w) => !selectedWordIds.has(w.id));
39535
+ segment.text = segment.words.map((w) => w.text).join(" ");
39536
+ const timedWords = segment.words.filter(
39537
+ (w) => w.start_time !== null && w.end_time !== null
39538
+ );
39539
+ if (timedWords.length > 0) {
39540
+ segment.start_time = Math.min(...timedWords.map((w) => w.start_time));
39541
+ segment.end_time = Math.max(...timedWords.map((w) => w.end_time));
39542
+ } else {
39543
+ segment.start_time = null;
39544
+ segment.end_time = null;
39545
+ }
39546
+ }
39547
+ return newSegments.filter((s) => s.words.length > 0);
39548
+ });
39549
+ setSelectedWordIds(/* @__PURE__ */ new Set());
39550
+ }, [selectedWordIds]);
39551
+ const handleWordClick = reactExports.useCallback((wordId, event) => {
39552
+ if (event.shiftKey || event.ctrlKey || event.metaKey) {
39553
+ setSelectedWordIds((prev2) => {
39554
+ const newSet = new Set(prev2);
39555
+ if (newSet.has(wordId)) {
39556
+ newSet.delete(wordId);
39557
+ } else {
39558
+ newSet.add(wordId);
39559
+ }
39560
+ return newSet;
39561
+ });
39562
+ } else {
39563
+ setSelectedWordIds(/* @__PURE__ */ new Set([wordId]));
39564
+ }
39565
+ }, []);
39566
+ const handleBackgroundClick = reactExports.useCallback(() => {
39567
+ setSelectedWordIds(/* @__PURE__ */ new Set());
39568
+ }, []);
39569
+ const handleWordTimingChange = reactExports.useCallback((wordId, newStartTime, newEndTime) => {
39570
+ setWorkingSegments((prevSegments) => {
39571
+ const newSegments = cloneSegments(prevSegments);
39572
+ for (const segment of newSegments) {
39573
+ const word = segment.words.find((w) => w.id === wordId);
39574
+ if (word) {
39575
+ word.start_time = Math.max(0, newStartTime);
39576
+ word.end_time = Math.max(word.start_time + 0.05, newEndTime);
39577
+ const timedWords = segment.words.filter(
39578
+ (w) => w.start_time !== null && w.end_time !== null
39579
+ );
39580
+ if (timedWords.length > 0) {
39581
+ segment.start_time = Math.min(...timedWords.map((w) => w.start_time));
39582
+ segment.end_time = Math.max(...timedWords.map((w) => w.end_time));
39583
+ }
39584
+ break;
39585
+ }
39586
+ }
39587
+ return newSegments;
39588
+ });
39589
+ }, []);
39590
+ const handleWordsMove = reactExports.useCallback((updates) => {
39591
+ setWorkingSegments((prevSegments) => {
39592
+ const newSegments = cloneSegments(prevSegments);
39593
+ const updateMap = new Map(updates.map((u) => [u.wordId, u]));
39594
+ for (const segment of newSegments) {
39595
+ for (const word of segment.words) {
39596
+ const update = updateMap.get(word.id);
39597
+ if (update) {
39598
+ word.start_time = update.newStartTime;
39599
+ word.end_time = update.newEndTime;
39600
+ }
39601
+ }
39602
+ const timedWords = segment.words.filter(
39603
+ (w) => w.start_time !== null && w.end_time !== null
39604
+ );
39605
+ if (timedWords.length > 0) {
39606
+ segment.start_time = Math.min(...timedWords.map((w) => w.start_time));
39607
+ segment.end_time = Math.max(...timedWords.map((w) => w.end_time));
39608
+ }
39609
+ }
39610
+ return newSegments;
39611
+ });
39612
+ }, []);
39613
+ const handleTimeBarClick = reactExports.useCallback((time) => {
39614
+ const newStart = Math.max(0, time - zoomSeconds / 2);
39615
+ setVisibleStartTime(Math.min(newStart, Math.max(0, audioDuration - zoomSeconds)));
39616
+ if (onPlaySegment) {
39617
+ onPlaySegment(time);
39618
+ setTimeout(() => {
39619
+ if (window.toggleAudioPlayback && window.isAudioPlaying) {
39620
+ window.toggleAudioPlayback();
39621
+ }
39622
+ }, 50);
39623
+ }
39624
+ }, [zoomSeconds, audioDuration, onPlaySegment]);
39625
+ const handleSelectionComplete = reactExports.useCallback((wordIds) => {
39626
+ setSelectedWordIds(new Set(wordIds));
39627
+ }, []);
39628
+ const handleKeyDown = reactExports.useCallback((e) => {
39629
+ if (e.code !== "Space") return;
39630
+ if (!isManualSyncing || isPaused) return;
39631
+ if (syncWordIndex < 0 || syncWordIndex >= allWords.length) return;
39632
+ e.preventDefault();
39633
+ e.stopPropagation();
39634
+ if (isSpacebarPressed) return;
39635
+ setIsSpacebarPressed(true);
39636
+ wordStartTimeRef.current = currentTimeRef.current;
39637
+ spacebarPressTimeRef.current = Date.now();
39638
+ const newWords = [...allWords];
39639
+ const currentWord = newWords[syncWordIndex];
39640
+ currentWord.start_time = currentTimeRef.current;
39641
+ if (syncWordIndex > 0) {
39642
+ const prevWord = newWords[syncWordIndex - 1];
39643
+ if (prevWord.start_time !== null && prevWord.end_time === null) {
39644
+ const gap2 = currentTimeRef.current - prevWord.start_time;
39645
+ if (gap2 > 1) {
39646
+ prevWord.end_time = prevWord.start_time + 0.5;
39647
+ } else {
39648
+ prevWord.end_time = currentTimeRef.current - 5e-3;
39649
+ }
39650
+ }
39651
+ }
39652
+ updateWords(newWords);
39653
+ }, [isManualSyncing, isPaused, syncWordIndex, allWords, isSpacebarPressed, updateWords]);
39654
+ const handleKeyUp = reactExports.useCallback((e) => {
39655
+ if (e.code !== "Space") return;
39656
+ if (!isManualSyncing || isPaused) return;
39657
+ if (!isSpacebarPressed) return;
39658
+ e.preventDefault();
39659
+ e.stopPropagation();
39660
+ setIsSpacebarPressed(false);
39661
+ const pressDuration = spacebarPressTimeRef.current ? Date.now() - spacebarPressTimeRef.current : 0;
39662
+ const isTap = pressDuration < 200;
39663
+ const newWords = [...allWords];
39664
+ const currentWord = newWords[syncWordIndex];
39665
+ if (isTap) {
39666
+ currentWord.end_time = (wordStartTimeRef.current || currentTimeRef.current) + 0.5;
39667
+ } else {
39668
+ currentWord.end_time = currentTimeRef.current;
39669
+ }
39670
+ updateWords(newWords);
39671
+ if (syncWordIndex < allWords.length - 1) {
39672
+ setSyncWordIndex(syncWordIndex + 1);
39673
+ } else {
39674
+ setIsManualSyncing(false);
39675
+ setSyncWordIndex(-1);
39676
+ handleStopAudio();
39677
+ }
39678
+ wordStartTimeRef.current = null;
39679
+ spacebarPressTimeRef.current = null;
39680
+ }, [isManualSyncing, isPaused, isSpacebarPressed, syncWordIndex, allWords, updateWords, handleStopAudio]);
39681
+ const handleSpacebar = reactExports.useCallback((e) => {
39682
+ if (e.type === "keydown") {
39683
+ handleKeyDown(e);
39684
+ } else if (e.type === "keyup") {
39685
+ handleKeyUp(e);
39686
+ }
39687
+ }, [handleKeyDown, handleKeyUp]);
39688
+ const spacebarHandlerRef = reactExports.useRef(handleSpacebar);
39689
+ spacebarHandlerRef.current = handleSpacebar;
39690
+ reactExports.useEffect(() => {
39691
+ const handler = (e) => {
39692
+ if (e.code === "Space") {
39693
+ e.preventDefault();
39694
+ e.stopPropagation();
39695
+ spacebarHandlerRef.current(e);
39696
+ }
39697
+ };
39698
+ setModalSpacebarHandler(() => handler);
39699
+ return () => {
39700
+ setModalSpacebarHandler(void 0);
39701
+ };
39702
+ }, [setModalSpacebarHandler]);
39703
+ const handleSave = reactExports.useCallback(() => {
39704
+ onSave(workingSegments);
39705
+ }, [workingSegments, onSave]);
39706
+ const stats = reactExports.useMemo(() => {
39707
+ const total = allWords.length;
39708
+ const synced = allWords.filter(
39709
+ (w) => w.start_time !== null && w.end_time !== null
39710
+ ).length;
39711
+ return { total, synced, remaining: total - synced };
39712
+ }, [allWords]);
39713
+ const getInstructionText = reactExports.useCallback(() => {
39714
+ if (isManualSyncing) {
39715
+ if (isSpacebarPressed) {
39716
+ return { primary: "⏱️ Holding... release when word ends", secondary: "Release spacebar when the word finishes" };
39717
+ }
39718
+ if (stats.remaining === 0) {
39719
+ return { primary: "✅ All words synced!", secondary: 'Click "Stop Sync" then "Apply" to save' };
39720
+ }
39721
+ return { primary: "👆 Press SPACEBAR when you hear each word", secondary: "Tap for short words, hold for longer words" };
39722
+ }
39723
+ if (stats.synced === 0) {
39724
+ return { primary: 'Click "Start Sync" to begin timing words', secondary: "Audio will play and you'll tap spacebar for each word" };
39725
+ }
39726
+ if (stats.remaining > 0) {
39727
+ return { primary: `${stats.remaining} words remaining to sync`, secondary: 'Click "Start Sync" to continue, or "Unsync from Cursor" to re-sync from a point' };
39728
+ }
39729
+ return { primary: "✅ All words synced!", secondary: 'Click "Apply" to save changes, or make adjustments first' };
39730
+ }, [isManualSyncing, isSpacebarPressed, stats.synced, stats.remaining]);
39731
+ const instruction = getInstructionText();
39732
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { display: "flex", flexDirection: "column", height: "100%", gap: 1 }, children: [
39733
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { sx: { display: "flex", justifyContent: "flex-end", alignItems: "center", height: 24 }, children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Typography, { variant: "body2", color: "text.secondary", children: [
39734
+ stats.synced,
39735
+ " / ",
39736
+ stats.total,
39737
+ " words synced",
39738
+ stats.remaining > 0 && ` (${stats.remaining} remaining)`
39739
+ ] }) }),
39740
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
39741
+ Box,
39742
+ {
39743
+ sx: {
39744
+ height: 56,
39745
+ flexShrink: 0
39746
+ },
39747
+ children: /* @__PURE__ */ jsxRuntimeExports.jsxs(
39748
+ Paper,
39749
+ {
39750
+ sx: {
39751
+ p: 1.5,
39752
+ height: "100%",
39753
+ bgcolor: isManualSyncing ? "info.main" : "grey.100",
39754
+ color: isManualSyncing ? "info.contrastText" : "text.primary",
39755
+ display: "flex",
39756
+ flexDirection: "column",
39757
+ justifyContent: "center",
39758
+ overflow: "hidden",
39759
+ boxSizing: "border-box"
39760
+ },
39761
+ children: [
39762
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "body2", sx: { fontWeight: 500, lineHeight: 1.3 }, children: instruction.primary }),
39763
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "caption", sx: { opacity: 0.85, display: "block", lineHeight: 1.3 }, children: instruction.secondary })
39764
+ ]
39765
+ }
39766
+ )
39767
+ }
39768
+ ),
39769
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { sx: { height: 88, flexShrink: 0 }, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
39770
+ SyncControls,
39771
+ {
39772
+ isManualSyncing,
39773
+ isPaused,
39774
+ onStartSync: handleStartSync,
39775
+ onPauseSync: handlePauseSync,
39776
+ onResumeSync: handleResumeSync,
39777
+ onClearSync: handleClearSync,
39778
+ onEditLyrics: handleEditLyrics,
39779
+ onPlay: handlePlayAudio,
39780
+ onStop: handleStopAudio,
39781
+ isPlaying,
39782
+ hasSelectedWords: selectedWordIds.size > 0,
39783
+ selectedWordCount: selectedWordIds.size,
39784
+ onUnsyncFromCursor: handleUnsyncFromCursor,
39785
+ onEditSelectedWord: handleEditSelectedWord,
39786
+ onDeleteSelected: handleDeleteSelected,
39787
+ canUnsyncFromCursor
39788
+ }
39789
+ ) }),
39790
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { sx: { height: 44, flexShrink: 0 }, children: isManualSyncing && /* @__PURE__ */ jsxRuntimeExports.jsx(
39791
+ UpcomingWordsBar,
39792
+ {
39793
+ words: allWords,
39794
+ syncWordIndex,
39795
+ isManualSyncing
39796
+ }
39797
+ ) }),
39798
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { sx: { flexGrow: 1, minHeight: 200 }, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
39799
+ TimelineCanvas,
39800
+ {
39801
+ words: allWords,
39802
+ segments: workingSegments,
39803
+ visibleStartTime,
39804
+ visibleEndTime,
39805
+ currentTime,
39806
+ selectedWordIds,
39807
+ onWordClick: handleWordClick,
39808
+ onBackgroundClick: handleBackgroundClick,
39809
+ onTimeBarClick: handleTimeBarClick,
39810
+ onSelectionComplete: handleSelectionComplete,
39811
+ onWordTimingChange: handleWordTimingChange,
39812
+ onWordsMove: handleWordsMove,
39813
+ syncWordIndex,
39814
+ isManualSyncing,
39815
+ onScrollChange: handleScrollChange,
39816
+ audioDuration,
39817
+ zoomSeconds,
39818
+ height: 200
39819
+ }
39820
+ ) }),
39821
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: 2, px: 2 }, children: [
39822
+ /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomInIcon, { color: "action", fontSize: "small" }),
39823
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
39824
+ Slider,
39825
+ {
39826
+ value: sliderValue,
39827
+ onChange: handleZoomChange,
39828
+ min: 0,
39829
+ max: ZOOM_STEPS,
39830
+ step: 1,
39831
+ sx: { flexGrow: 1 },
39832
+ disabled: isManualSyncing && !isPaused
39833
+ }
39834
+ ),
39835
+ /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomOutIcon, { color: "action", fontSize: "small" }),
39836
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(Typography, { variant: "caption", color: "text.secondary", sx: { minWidth: 60 }, children: [
39837
+ zoomSeconds.toFixed(1),
39838
+ "s view"
39839
+ ] })
39840
+ ] }),
39841
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { display: "flex", justifyContent: "flex-end", gap: 2, pt: 2, borderTop: 1, borderColor: "divider" }, children: [
39842
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { onClick: onCancel, color: "inherit", children: "Cancel" }),
39843
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
39844
+ Button,
39845
+ {
39846
+ onClick: handleSave,
39847
+ variant: "contained",
39848
+ color: "primary",
39849
+ disabled: isManualSyncing && !isPaused,
39850
+ children: "Apply"
39851
+ }
39852
+ )
39853
+ ] }),
39854
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
39855
+ Dialog,
39856
+ {
39857
+ open: showEditLyricsModal,
39858
+ onClose: () => setShowEditLyricsModal(false),
39859
+ maxWidth: "md",
39860
+ fullWidth: true,
39861
+ children: [
39862
+ /* @__PURE__ */ jsxRuntimeExports.jsx(DialogTitle, { children: "Edit Lyrics" }),
39863
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(DialogContent, { children: [
39864
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Alert, { severity: "warning", sx: { mb: 2 }, children: "Editing lyrics will reset all timing data. You will need to re-sync the entire song." }),
39865
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
39866
+ TextField,
39867
+ {
39868
+ multiline: true,
39869
+ rows: 15,
39870
+ fullWidth: true,
39871
+ value: editLyricsText,
39872
+ onChange: (e) => setEditLyricsText(e.target.value),
39873
+ placeholder: "Enter lyrics, one line per segment..."
39874
+ }
39875
+ )
39876
+ ] }),
39877
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(DialogActions, { children: [
39878
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { onClick: () => setShowEditLyricsModal(false), children: "Cancel" }),
39879
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { onClick: handleSaveEditedLyrics, variant: "contained", color: "warning", children: "Save & Reset Timing" })
39880
+ ] })
39881
+ ]
39882
+ }
39883
+ ),
39884
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
39885
+ Dialog,
39886
+ {
39887
+ open: showEditWordModal,
39888
+ onClose: () => setShowEditWordModal(false),
39889
+ maxWidth: "xs",
39890
+ fullWidth: true,
39891
+ children: [
39892
+ /* @__PURE__ */ jsxRuntimeExports.jsx(DialogTitle, { children: "Edit Word" }),
39893
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(DialogContent, { children: [
39894
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "body2", color: "text.secondary", sx: { mb: 2 }, children: "Edit the word text. Enter multiple words separated by spaces to split." }),
39895
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
39896
+ TextField,
39897
+ {
39898
+ fullWidth: true,
39899
+ value: editWordText,
39900
+ onChange: (e) => setEditWordText(e.target.value),
39901
+ autoFocus: true,
39902
+ onKeyDown: (e) => {
39903
+ if (e.key === "Enter") {
39904
+ handleSaveEditedWord();
39905
+ }
39906
+ }
39907
+ }
39908
+ )
39909
+ ] }),
39910
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(DialogActions, { children: [
39911
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { onClick: () => setShowEditWordModal(false), children: "Cancel" }),
39912
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { onClick: handleSaveEditedWord, variant: "contained", children: "Save" })
39913
+ ] })
39914
+ ]
39915
+ }
39916
+ )
39917
+ ] });
39918
+ });
38431
39919
  function ReplaceAllLyricsModal({
38432
39920
  open,
38433
39921
  onClose,
38434
39922
  onSave,
38435
39923
  onPlaySegment,
38436
39924
  currentTime = 0,
38437
- setModalSpacebarHandler
39925
+ setModalSpacebarHandler,
39926
+ existingSegments = []
38438
39927
  }) {
39928
+ const [mode, setMode] = reactExports.useState("selection");
38439
39929
  const [inputText, setInputText] = reactExports.useState("");
38440
- const [isReplaced, setIsReplaced] = reactExports.useState(false);
38441
- const [globalSegment, setGlobalSegment] = reactExports.useState(null);
38442
- const [originalSegments, setOriginalSegments] = reactExports.useState([]);
38443
- const [currentSegments, setCurrentSegments] = reactExports.useState([]);
38444
- const getAudioDuration = reactExports.useCallback(() => {
38445
- if (window.getAudioDuration) {
38446
- const duration2 = window.getAudioDuration();
38447
- return duration2 > 0 ? duration2 : 600;
39930
+ const [newSegments, setNewSegments] = reactExports.useState([]);
39931
+ reactExports.useEffect(() => {
39932
+ if (open) {
39933
+ setMode("selection");
39934
+ setInputText("");
39935
+ setNewSegments([]);
38448
39936
  }
38449
- return 600;
38450
- }, []);
39937
+ }, [open]);
38451
39938
  const parseInfo = reactExports.useMemo(() => {
38452
39939
  if (!inputText.trim()) return { lines: 0, words: 0 };
38453
39940
  const lines = inputText.trim().split("\n").filter((line2) => line2.trim().length > 0);
@@ -38459,295 +39946,101 @@ function ReplaceAllLyricsModal({
38459
39946
  const processLyrics = reactExports.useCallback(() => {
38460
39947
  if (!inputText.trim()) return;
38461
39948
  const lines = inputText.trim().split("\n").filter((line2) => line2.trim().length > 0);
38462
- const newSegments = [];
38463
- const allWords = [];
39949
+ const segments = [];
38464
39950
  lines.forEach((line2) => {
38465
39951
  const words = line2.trim().split(/\s+/).filter((word) => word.length > 0);
38466
- const segmentWords = [];
38467
- words.forEach((wordText) => {
38468
- const word = {
38469
- id: nanoid(),
38470
- text: wordText,
38471
- start_time: null,
38472
- end_time: null,
38473
- confidence: 1,
38474
- created_during_correction: true
38475
- };
38476
- segmentWords.push(word);
38477
- allWords.push(word);
38478
- });
38479
- const segment = {
39952
+ const segmentWords = words.map((wordText) => ({
38480
39953
  id: nanoid(),
38481
- text: line2.trim(),
38482
- words: segmentWords,
39954
+ text: wordText,
38483
39955
  start_time: null,
38484
- end_time: null
38485
- };
38486
- newSegments.push(segment);
38487
- });
38488
- const audioDuration = getAudioDuration();
38489
- const endTime = Math.max(audioDuration, 3600);
38490
- console.log("ReplaceAllLyricsModal - Creating global segment", {
38491
- audioDuration,
38492
- endTime,
38493
- wordCount: allWords.length
38494
- });
38495
- const globalSegment2 = {
38496
- id: "global-replacement",
38497
- text: allWords.map((w) => w.text).join(" "),
38498
- words: allWords,
38499
- start_time: 0,
38500
- end_time: endTime
38501
- };
38502
- setCurrentSegments(newSegments);
38503
- setOriginalSegments(JSON.parse(JSON.stringify(newSegments)));
38504
- setGlobalSegment(globalSegment2);
38505
- setIsReplaced(true);
38506
- }, [inputText, getAudioDuration]);
38507
- const handlePasteFromClipboard = reactExports.useCallback(async () => {
38508
- try {
38509
- const text = await navigator.clipboard.readText();
38510
- setInputText(text);
38511
- } catch (error) {
38512
- console.error("Failed to read from clipboard:", error);
38513
- alert("Failed to read from clipboard. Please paste manually.");
38514
- }
38515
- }, []);
38516
- const updateSegment2 = reactExports.useCallback((newWords) => {
38517
- if (!globalSegment) return;
38518
- const validStartTimes = newWords.map((w) => w.start_time).filter((t) => t !== null);
38519
- const validEndTimes = newWords.map((w) => w.end_time).filter((t) => t !== null);
38520
- const segmentStartTime = validStartTimes.length > 0 ? Math.min(...validStartTimes) : null;
38521
- const segmentEndTime = validEndTimes.length > 0 ? Math.max(...validEndTimes) : null;
38522
- const updatedGlobalSegment = {
38523
- ...globalSegment,
38524
- words: newWords,
38525
- text: newWords.map((w) => w.text).join(" "),
38526
- start_time: segmentStartTime,
38527
- end_time: segmentEndTime
38528
- };
38529
- setGlobalSegment(updatedGlobalSegment);
38530
- const updatedSegments = currentSegments.map((segment) => {
38531
- const segmentWordsWithTiming = segment.words.map((segmentWord) => {
38532
- const globalWord = newWords.find((w) => w.id === segmentWord.id);
38533
- return globalWord || segmentWord;
38534
- });
38535
- const wordsWithTiming = segmentWordsWithTiming.filter(
38536
- (w) => w.start_time !== null && w.end_time !== null
38537
- );
38538
- if (wordsWithTiming.length === segmentWordsWithTiming.length && wordsWithTiming.length > 0) {
38539
- const segmentStart = Math.min(...wordsWithTiming.map((w) => w.start_time));
38540
- const segmentEnd = Math.max(...wordsWithTiming.map((w) => w.end_time));
38541
- return {
38542
- ...segment,
38543
- words: segmentWordsWithTiming,
38544
- start_time: segmentStart,
38545
- end_time: segmentEnd
38546
- };
38547
- } else {
38548
- return {
38549
- ...segment,
38550
- words: segmentWordsWithTiming
38551
- };
38552
- }
39956
+ end_time: null,
39957
+ confidence: 1,
39958
+ created_during_correction: true
39959
+ }));
39960
+ segments.push({
39961
+ id: nanoid(),
39962
+ text: line2.trim(),
39963
+ words: segmentWords,
39964
+ start_time: null,
39965
+ end_time: null
39966
+ });
38553
39967
  });
38554
- setCurrentSegments(updatedSegments);
38555
- }, [globalSegment, currentSegments]);
38556
- const {
38557
- isManualSyncing,
38558
- isPaused,
38559
- syncWordIndex,
38560
- startManualSync,
38561
- pauseManualSync,
38562
- resumeManualSync,
38563
- cleanupManualSync,
38564
- handleSpacebar,
38565
- isSpacebarPressed
38566
- } = useManualSync({
38567
- editedSegment: globalSegment,
38568
- currentTime,
38569
- onPlaySegment,
38570
- updateSegment: updateSegment2
38571
- });
38572
- const handleWordUpdate = reactExports.useCallback((wordIndex, updates) => {
38573
- var _a;
38574
- if (!globalSegment) return;
38575
- if (isManualSyncing && !isPaused) {
38576
- console.log("ReplaceAllLyricsModal - Ignoring word update during active manual sync");
38577
- return;
39968
+ setNewSegments(segments);
39969
+ setMode("resync");
39970
+ }, [inputText]);
39971
+ const handlePasteFromClipboard = reactExports.useCallback(async () => {
39972
+ try {
39973
+ const text = await navigator.clipboard.readText();
39974
+ setInputText(text);
39975
+ } catch (error) {
39976
+ console.error("Failed to read from clipboard:", error);
39977
+ alert("Failed to read from clipboard. Please paste manually.");
38578
39978
  }
38579
- console.log("ReplaceAllLyricsModal - Manual word update", {
38580
- wordIndex,
38581
- wordText: (_a = globalSegment.words[wordIndex]) == null ? void 0 : _a.text,
38582
- updates,
38583
- isManualSyncing,
38584
- isPaused
38585
- });
38586
- const newWords = [...globalSegment.words];
38587
- newWords[wordIndex] = {
38588
- ...newWords[wordIndex],
38589
- ...updates
38590
- };
38591
- updateSegment2(newWords);
38592
- }, [globalSegment, updateSegment2, isManualSyncing, isPaused]);
38593
- const handleUnsyncWord = reactExports.useCallback((wordIndex) => {
38594
- var _a;
38595
- if (!globalSegment) return;
38596
- console.log("ReplaceAllLyricsModal - Un-syncing word", {
38597
- wordIndex,
38598
- wordText: (_a = globalSegment.words[wordIndex]) == null ? void 0 : _a.text
38599
- });
38600
- const newWords = [...globalSegment.words];
38601
- newWords[wordIndex] = {
38602
- ...newWords[wordIndex],
38603
- start_time: null,
38604
- end_time: null
38605
- };
38606
- updateSegment2(newWords);
38607
- }, [globalSegment, updateSegment2]);
39979
+ }, []);
38608
39980
  const handleClose = reactExports.useCallback(() => {
38609
- cleanupManualSync();
39981
+ setMode("selection");
38610
39982
  setInputText("");
38611
- setIsReplaced(false);
38612
- setGlobalSegment(null);
38613
- setOriginalSegments([]);
38614
- setCurrentSegments([]);
39983
+ setNewSegments([]);
38615
39984
  onClose();
38616
- }, [onClose, cleanupManualSync]);
38617
- const handleSave = reactExports.useCallback(() => {
38618
- if (!globalSegment || !currentSegments.length) return;
38619
- const finalSegments = [];
38620
- let wordIndex = 0;
38621
- currentSegments.forEach((segment) => {
38622
- const originalWordCount = segment.words.length;
38623
- const segmentWords = globalSegment.words.slice(wordIndex, wordIndex + originalWordCount);
38624
- wordIndex += originalWordCount;
38625
- if (segmentWords.length > 0) {
38626
- const validStartTimes = segmentWords.map((w) => w.start_time).filter((t) => t !== null);
38627
- const validEndTimes = segmentWords.map((w) => w.end_time).filter((t) => t !== null);
38628
- const segmentStartTime = validStartTimes.length > 0 ? Math.min(...validStartTimes) : null;
38629
- const segmentEndTime = validEndTimes.length > 0 ? Math.max(...validEndTimes) : null;
38630
- finalSegments.push({
38631
- ...segment,
38632
- words: segmentWords,
38633
- text: segmentWords.map((w) => w.text).join(" "),
38634
- start_time: segmentStartTime,
38635
- end_time: segmentEndTime
38636
- });
38637
- }
38638
- });
38639
- console.log("ReplaceAllLyricsModal - Saving new segments:", {
38640
- originalSegmentCount: currentSegments.length,
38641
- finalSegmentCount: finalSegments.length,
38642
- totalWords: finalSegments.reduce((count, seg) => count + seg.words.length, 0)
38643
- });
38644
- onSave(finalSegments);
39985
+ }, [onClose]);
39986
+ const handleSave = reactExports.useCallback((segments) => {
39987
+ onSave(segments);
38645
39988
  handleClose();
38646
- }, [globalSegment, currentSegments, onSave, handleClose]);
38647
- const handleReset = reactExports.useCallback(() => {
38648
- if (!originalSegments.length) return;
38649
- console.log("ReplaceAllLyricsModal - Resetting to original state");
38650
- const resetWords = originalSegments.flatMap(
38651
- (segment) => segment.words.map((word) => ({
38652
- ...word,
38653
- start_time: null,
38654
- end_time: null
38655
- }))
38656
- );
38657
- const audioDuration = getAudioDuration();
38658
- const resetGlobalSegment = {
38659
- id: "global-replacement",
38660
- text: resetWords.map((w) => w.text).join(" "),
38661
- words: resetWords,
38662
- start_time: 0,
38663
- end_time: Math.max(audioDuration, 3600)
38664
- // At least 1 hour to prevent auto-stop
38665
- };
38666
- const resetCurrentSegments = originalSegments.map((segment) => ({
38667
- ...segment,
38668
- words: segment.words.map((word) => ({
38669
- ...word,
38670
- start_time: null,
38671
- end_time: null
38672
- })),
38673
- start_time: null,
38674
- end_time: null
38675
- }));
38676
- setGlobalSegment(resetGlobalSegment);
38677
- setCurrentSegments(resetCurrentSegments);
38678
- }, [originalSegments, getAudioDuration]);
38679
- const spacebarHandlerRef = reactExports.useRef(handleSpacebar);
38680
- spacebarHandlerRef.current = handleSpacebar;
38681
- reactExports.useEffect(() => {
38682
- if (open && isReplaced) {
38683
- console.log("ReplaceAllLyricsModal - Setting up spacebar handler");
38684
- const handleKeyEvent = (e) => {
38685
- if (e.code === "Space") {
38686
- console.log("ReplaceAllLyricsModal - Spacebar captured in modal");
38687
- e.preventDefault();
38688
- e.stopPropagation();
38689
- spacebarHandlerRef.current(e);
38690
- }
38691
- };
38692
- setModalSpacebarHandler(() => handleKeyEvent);
38693
- return () => {
38694
- if (!open) {
38695
- console.log("ReplaceAllLyricsModal - Clearing spacebar handler");
38696
- setModalSpacebarHandler(void 0);
38697
- }
38698
- };
38699
- } else if (open) {
38700
- setModalSpacebarHandler(void 0);
38701
- }
38702
- }, [open, isReplaced, setModalSpacebarHandler]);
38703
- const timeRange = reactExports.useMemo(() => {
38704
- const audioDuration = getAudioDuration();
38705
- return { start: 0, end: audioDuration };
38706
- }, [getAudioDuration]);
38707
- const segmentProgressProps = reactExports.useMemo(() => ({
38708
- currentSegments,
38709
- globalSegment,
38710
- syncWordIndex
38711
- }), [currentSegments, globalSegment, syncWordIndex]);
38712
- return /* @__PURE__ */ jsxRuntimeExports.jsxs(
38713
- Dialog,
38714
- {
38715
- open,
38716
- onClose: handleClose,
38717
- maxWidth: false,
38718
- fullWidth: true,
38719
- onKeyDown: (e) => {
38720
- if (e.key === "Enter" && !e.shiftKey && isReplaced) {
38721
- e.preventDefault();
38722
- handleSave();
38723
- }
38724
- },
38725
- PaperProps: {
38726
- sx: {
38727
- height: "90vh",
38728
- margin: "5vh 2vh",
38729
- maxWidth: "calc(100vw - 4vh)",
38730
- width: "calc(100vw - 4vh)"
38731
- }
38732
- },
38733
- children: [
38734
- /* @__PURE__ */ jsxRuntimeExports.jsxs(DialogTitle, { sx: { display: "flex", alignItems: "center", gap: 1 }, children: [
38735
- /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { sx: { flex: 1 }, children: "Replace All Lyrics" }),
38736
- /* @__PURE__ */ jsxRuntimeExports.jsx(IconButton, { onClick: handleClose, sx: { ml: "auto" }, children: /* @__PURE__ */ jsxRuntimeExports.jsx(CloseIcon, {}) })
38737
- ] }),
38738
- /* @__PURE__ */ jsxRuntimeExports.jsx(
38739
- DialogContent,
38740
- {
38741
- dividers: true,
38742
- sx: {
38743
- display: "flex",
38744
- flexDirection: "column",
38745
- flexGrow: 1,
38746
- overflow: "hidden"
38747
- },
38748
- children: !isReplaced ? (
38749
- // Step 1: Input new lyrics
38750
- /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { display: "flex", flexDirection: "column", gap: 2, height: "100%" }, children: [
39989
+ }, [onSave, handleClose]);
39990
+ const handleSelectReplace = reactExports.useCallback(() => {
39991
+ setMode("replace");
39992
+ }, []);
39993
+ const handleSelectResync = reactExports.useCallback(() => {
39994
+ setMode("resync");
39995
+ }, []);
39996
+ const handleBackToSelection = reactExports.useCallback(() => {
39997
+ setMode("selection");
39998
+ setInputText("");
39999
+ setNewSegments([]);
40000
+ }, []);
40001
+ const segmentsForSync = mode === "resync" && newSegments.length > 0 ? newSegments : existingSegments;
40002
+ const hasExistingLyrics = existingSegments.length > 0 && existingSegments.some((s) => s.words.length > 0);
40003
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
40004
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
40005
+ ModeSelectionModal,
40006
+ {
40007
+ open: open && mode === "selection",
40008
+ onClose: handleClose,
40009
+ onSelectReplace: handleSelectReplace,
40010
+ onSelectResync: handleSelectResync,
40011
+ hasExistingLyrics
40012
+ }
40013
+ ),
40014
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
40015
+ Dialog,
40016
+ {
40017
+ open: open && mode === "replace",
40018
+ onClose: handleClose,
40019
+ maxWidth: "md",
40020
+ fullWidth: true,
40021
+ PaperProps: {
40022
+ sx: {
40023
+ height: "80vh",
40024
+ maxHeight: "80vh"
40025
+ }
40026
+ },
40027
+ children: [
40028
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(DialogTitle, { sx: { display: "flex", alignItems: "center", gap: 1 }, children: [
40029
+ /* @__PURE__ */ jsxRuntimeExports.jsx(IconButton, { onClick: handleBackToSelection, size: "small", children: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowBackIcon, {}) }),
40030
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { sx: { flex: 1 }, children: "Replace All Lyrics" }),
40031
+ /* @__PURE__ */ jsxRuntimeExports.jsx(IconButton, { onClick: handleClose, sx: { ml: "auto" }, children: /* @__PURE__ */ jsxRuntimeExports.jsx(CloseIcon, {}) })
40032
+ ] }),
40033
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
40034
+ DialogContent,
40035
+ {
40036
+ dividers: true,
40037
+ sx: {
40038
+ display: "flex",
40039
+ flexDirection: "column",
40040
+ flexGrow: 1,
40041
+ overflow: "hidden"
40042
+ },
40043
+ children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { display: "flex", flexDirection: "column", gap: 2, height: "100%" }, children: [
38751
40044
  /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "h6", gutterBottom: true, children: "Paste your new lyrics below:" }),
38752
40045
  /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "body2", color: "text.secondary", gutterBottom: true, children: "Each line will become a separate segment. Words will be separated by spaces." }),
38753
40046
  /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { display: "flex", gap: 2, mb: 2 }, children: [
@@ -38788,185 +40081,95 @@ function ReplaceAllLyricsModal({
38788
40081
  }
38789
40082
  }
38790
40083
  }
38791
- ),
38792
- /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { sx: { display: "flex", justifyContent: "flex-end", gap: 2 }, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
38793
- Button,
38794
- {
38795
- variant: "contained",
38796
- onClick: processLyrics,
38797
- disabled: !inputText.trim(),
38798
- startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(AutoFixHighIcon, {}),
38799
- children: "Replace All Lyrics"
38800
- }
38801
- ) })
38802
- ] })
38803
- ) : (
38804
- // Step 2: Manual sync interface
38805
- /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { display: "flex", flexDirection: "column", height: "100%", gap: 2 }, children: [
38806
- /* @__PURE__ */ jsxRuntimeExports.jsxs(Paper, { sx: { p: 2, bgcolor: "background.paper" }, children: [
38807
- /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "h6", gutterBottom: true, children: "Lyrics Replaced Successfully" }),
38808
- /* @__PURE__ */ jsxRuntimeExports.jsxs(Typography, { variant: "body2", color: "text.secondary", children: [
38809
- "Created ",
38810
- currentSegments.length,
38811
- " segments with ",
38812
- globalSegment == null ? void 0 : globalSegment.words.length,
38813
- " words total. Use Manual Sync to set timing for all words."
38814
- ] })
38815
- ] }),
38816
- /* @__PURE__ */ jsxRuntimeExports.jsx(Divider, {}),
38817
- globalSegment && /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { display: "flex", gap: 2, flexGrow: 1, minHeight: 0 }, children: [
38818
- /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { sx: { flex: 2, display: "flex", flexDirection: "column", minHeight: 0 }, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
38819
- EditTimelineSection,
38820
- {
38821
- words: globalSegment.words,
38822
- startTime: timeRange.start,
38823
- endTime: timeRange.end,
38824
- originalStartTime: 0,
38825
- originalEndTime: getAudioDuration(),
38826
- currentStartTime: globalSegment.start_time,
38827
- currentEndTime: globalSegment.end_time,
38828
- currentTime,
38829
- isManualSyncing,
38830
- syncWordIndex,
38831
- isSpacebarPressed,
38832
- onWordUpdate: handleWordUpdate,
38833
- onUnsyncWord: handleUnsyncWord,
38834
- onPlaySegment,
38835
- onStopAudio: () => {
38836
- if (window.toggleAudioPlayback && window.isAudioPlaying) {
38837
- window.toggleAudioPlayback();
38838
- }
38839
- },
38840
- startManualSync,
38841
- pauseManualSync,
38842
- resumeManualSync,
38843
- isPaused,
38844
- isGlobal: true,
38845
- defaultZoomLevel: 10,
38846
- isReplaceAllMode: true
38847
- }
38848
- ) }),
38849
- /* @__PURE__ */ jsxRuntimeExports.jsx(
38850
- SegmentProgressPanel,
38851
- {
38852
- currentSegments: segmentProgressProps.currentSegments,
38853
- globalSegment: segmentProgressProps.globalSegment,
38854
- syncWordIndex: segmentProgressProps.syncWordIndex
38855
- }
38856
- )
38857
- ] })
40084
+ )
38858
40085
  ] })
40086
+ }
40087
+ ),
40088
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(DialogActions, { children: [
40089
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { onClick: handleClose, color: "inherit", children: "Cancel" }),
40090
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
40091
+ Button,
40092
+ {
40093
+ variant: "contained",
40094
+ onClick: processLyrics,
40095
+ disabled: !inputText.trim(),
40096
+ startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(AutoFixHighIcon, {}),
40097
+ children: "Continue to Sync"
40098
+ }
38859
40099
  )
38860
- }
38861
- ),
38862
- /* @__PURE__ */ jsxRuntimeExports.jsx(DialogActions, { children: isReplaced && /* @__PURE__ */ jsxRuntimeExports.jsx(
38863
- EditActionBar,
38864
- {
38865
- onReset: handleReset,
38866
- onClose: handleClose,
38867
- onSave: handleSave,
38868
- editedSegment: globalSegment,
38869
- isGlobal: true
38870
- }
38871
- ) })
38872
- ]
38873
- }
38874
- );
38875
- }
38876
- const SegmentProgressItem = reactExports.memo(({
38877
- segment,
38878
- index,
38879
- isActive
38880
- }) => {
38881
- const wordsWithTiming = segment.words.filter(
38882
- (w) => w.start_time !== null && w.end_time !== null
38883
- ).length;
38884
- const totalWords = segment.words.length;
38885
- const isComplete = wordsWithTiming === totalWords;
38886
- return /* @__PURE__ */ jsxRuntimeExports.jsxs(
38887
- Paper,
38888
- {
38889
- ref: isActive ? (el) => {
38890
- if (el) {
38891
- el.scrollIntoView({
38892
- behavior: "smooth",
38893
- block: "center"
38894
- });
38895
- }
38896
- } : void 0,
38897
- sx: {
38898
- p: 1,
38899
- mb: 1,
38900
- bgcolor: isActive ? "primary.light" : isComplete ? "success.light" : "background.paper",
38901
- border: isActive ? 2 : 1,
38902
- borderColor: isActive ? "primary.main" : "divider"
38903
- },
38904
- children: [
38905
- /* @__PURE__ */ jsxRuntimeExports.jsxs(
38906
- Typography,
38907
- {
38908
- variant: "body2",
38909
- sx: {
38910
- fontWeight: isActive ? "bold" : "normal",
38911
- mb: 0.5
38912
- },
38913
- children: [
38914
- "Segment ",
38915
- index + 1,
38916
- ": ",
38917
- segment.text.slice(0, 50),
38918
- segment.text.length > 50 ? "..." : ""
38919
- ]
38920
- }
38921
- ),
38922
- /* @__PURE__ */ jsxRuntimeExports.jsxs(Typography, { variant: "caption", color: "text.secondary", children: [
38923
- wordsWithTiming,
38924
- "/",
38925
- totalWords,
38926
- " words synced",
38927
- isComplete && segment.start_time !== null && segment.end_time !== null && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
38928
- /* @__PURE__ */ jsxRuntimeExports.jsx("br", {}),
38929
- segment.start_time.toFixed(2),
38930
- "s - ",
38931
- segment.end_time.toFixed(2),
38932
- "s"
38933
40100
  ] })
38934
- ] })
38935
- ]
38936
- },
38937
- segment.id
38938
- );
38939
- });
38940
- const SegmentProgressPanel = reactExports.memo(({
38941
- currentSegments,
38942
- globalSegment,
38943
- syncWordIndex
38944
- }) => {
38945
- return /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { flex: 1, display: "flex", flexDirection: "column", minHeight: 0 }, children: [
38946
- /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "h6", gutterBottom: true, children: "Segment Progress" }),
38947
- /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { sx: {
38948
- overflow: "auto",
38949
- flexGrow: 1,
38950
- border: 1,
38951
- borderColor: "divider",
38952
- borderRadius: 1,
38953
- p: 1
38954
- }, children: currentSegments.map((segment, index) => {
38955
- const isActive = Boolean(
38956
- globalSegment && syncWordIndex >= 0 && syncWordIndex < globalSegment.words.length && globalSegment.words[syncWordIndex] && segment.words.some((w) => w.id === globalSegment.words[syncWordIndex].id)
38957
- );
38958
- return /* @__PURE__ */ jsxRuntimeExports.jsx(
38959
- SegmentProgressItem,
38960
- {
38961
- segment,
38962
- index,
38963
- isActive
40101
+ ]
40102
+ }
40103
+ ),
40104
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
40105
+ Dialog,
40106
+ {
40107
+ open: open && mode === "resync",
40108
+ onClose: handleClose,
40109
+ maxWidth: false,
40110
+ fullWidth: true,
40111
+ PaperProps: {
40112
+ sx: {
40113
+ height: "90vh",
40114
+ margin: "5vh 2vw",
40115
+ maxWidth: "calc(100vw - 4vw)",
40116
+ width: "calc(100vw - 4vw)"
40117
+ }
38964
40118
  },
38965
- segment.id
38966
- );
38967
- }) })
40119
+ children: [
40120
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(DialogTitle, { sx: { display: "flex", alignItems: "center", gap: 1 }, children: [
40121
+ /* @__PURE__ */ jsxRuntimeExports.jsx(IconButton, { onClick: handleBackToSelection, size: "small", children: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowBackIcon, {}) }),
40122
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { sx: { flex: 1 }, children: newSegments.length > 0 ? "Sync New Lyrics" : "Re-sync Existing Lyrics" }),
40123
+ /* @__PURE__ */ jsxRuntimeExports.jsx(IconButton, { onClick: handleClose, sx: { ml: "auto" }, children: /* @__PURE__ */ jsxRuntimeExports.jsx(CloseIcon, {}) })
40124
+ ] }),
40125
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
40126
+ DialogContent,
40127
+ {
40128
+ dividers: true,
40129
+ sx: {
40130
+ display: "flex",
40131
+ flexDirection: "column",
40132
+ flexGrow: 1,
40133
+ overflow: "hidden",
40134
+ p: 2
40135
+ },
40136
+ children: segmentsForSync.length > 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx(
40137
+ LyricsSynchronizer,
40138
+ {
40139
+ segments: segmentsForSync,
40140
+ currentTime,
40141
+ onPlaySegment,
40142
+ onSave: handleSave,
40143
+ onCancel: handleClose,
40144
+ setModalSpacebarHandler
40145
+ }
40146
+ ) : /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: {
40147
+ display: "flex",
40148
+ flexDirection: "column",
40149
+ alignItems: "center",
40150
+ justifyContent: "center",
40151
+ height: "100%",
40152
+ gap: 2
40153
+ }, children: [
40154
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "h6", color: "text.secondary", children: "No lyrics to sync" }),
40155
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "body2", color: "text.secondary", children: "Go back and paste new lyrics, or close this modal." }),
40156
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
40157
+ Button,
40158
+ {
40159
+ variant: "outlined",
40160
+ onClick: handleBackToSelection,
40161
+ startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowBackIcon, {}),
40162
+ children: "Back to Selection"
40163
+ }
40164
+ )
40165
+ ] })
40166
+ }
40167
+ )
40168
+ ]
40169
+ }
40170
+ )
38968
40171
  ] });
38969
- });
40172
+ }
38970
40173
  const ANNOTATION_TYPES = [
38971
40174
  {
38972
40175
  value: "SOUND_ALIKE",
@@ -39866,7 +41069,7 @@ function AudioPlayer({ apiClient, onTimeUpdate, audioHash }) {
39866
41069
  audioRef.current.currentTime = time;
39867
41070
  setCurrentTime(time);
39868
41071
  };
39869
- const formatTime = (seconds) => {
41072
+ const formatTime2 = (seconds) => {
39870
41073
  const mins = Math.floor(seconds / 60);
39871
41074
  const secs = Math.floor(seconds % 60);
39872
41075
  return `${mins}:${secs.toString().padStart(2, "0")}`;
@@ -39918,7 +41121,7 @@ function AudioPlayer({ apiClient, onTimeUpdate, audioHash }) {
39918
41121
  children: isPlaying ? /* @__PURE__ */ jsxRuntimeExports.jsx(PauseIcon, { fontSize: "small" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(PlayArrowIcon, { fontSize: "small" })
39919
41122
  }
39920
41123
  ),
39921
- /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "body2", sx: { minWidth: 32, fontSize: "0.75rem" }, children: formatTime(currentTime) }),
41124
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "body2", sx: { minWidth: 32, fontSize: "0.75rem" }, children: formatTime2(currentTime) }),
39922
41125
  /* @__PURE__ */ jsxRuntimeExports.jsx(
39923
41126
  Slider,
39924
41127
  {
@@ -39940,7 +41143,7 @@ function AudioPlayer({ apiClient, onTimeUpdate, audioHash }) {
39940
41143
  }
39941
41144
  }
39942
41145
  ),
39943
- /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "body2", sx: { minWidth: 32, fontSize: "0.75rem" }, children: formatTime(duration2) })
41146
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "body2", sx: { minWidth: 32, fontSize: "0.75rem" }, children: formatTime2(duration2) })
39944
41147
  ] });
39945
41148
  }
39946
41149
  function Header({
@@ -39964,7 +41167,9 @@ function Header({
39964
41167
  onUndo,
39965
41168
  onRedo,
39966
41169
  canUndo,
39967
- canRedo
41170
+ canRedo,
41171
+ annotationsEnabled = true,
41172
+ onAnnotationsToggle
39968
41173
  }) {
39969
41174
  var _a, _b, _c;
39970
41175
  const theme2 = useTheme();
@@ -40011,17 +41216,34 @@ function Header({
40011
41216
  mb: 1
40012
41217
  }, children: [
40013
41218
  /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "h4", sx: { fontSize: isMobile ? "1.3rem" : "1.5rem" }, children: "Nomad Karaoke: Lyrics Transcription Review" }),
40014
- isReadOnly && /* @__PURE__ */ jsxRuntimeExports.jsx(
40015
- Button,
40016
- {
40017
- variant: "outlined",
40018
- size: "small",
40019
- startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(UploadFileIcon, {}),
40020
- onClick: onFileLoad,
40021
- fullWidth: isMobile,
40022
- children: "Load File"
40023
- }
40024
- )
41219
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: 1 }, children: [
41220
+ !isReadOnly && onAnnotationsToggle && /* @__PURE__ */ jsxRuntimeExports.jsx(Tooltip, { title: annotationsEnabled ? "Click to disable annotation prompts when editing" : "Click to enable annotation prompts when editing", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
41221
+ Chip,
41222
+ {
41223
+ icon: /* @__PURE__ */ jsxRuntimeExports.jsx(RateReviewIcon, {}),
41224
+ label: annotationsEnabled ? "Feedback On" : "Feedback Off",
41225
+ onClick: () => onAnnotationsToggle(!annotationsEnabled),
41226
+ color: annotationsEnabled ? "primary" : "default",
41227
+ variant: annotationsEnabled ? "filled" : "outlined",
41228
+ size: "small",
41229
+ sx: {
41230
+ cursor: "pointer",
41231
+ "& .MuiChip-icon": { fontSize: "1rem" }
41232
+ }
41233
+ }
41234
+ ) }),
41235
+ isReadOnly && /* @__PURE__ */ jsxRuntimeExports.jsx(
41236
+ Button,
41237
+ {
41238
+ variant: "outlined",
41239
+ size: "small",
41240
+ startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(UploadFileIcon, {}),
41241
+ onClick: onFileLoad,
41242
+ fullWidth: isMobile,
41243
+ children: "Load File"
41244
+ }
41245
+ )
41246
+ ] })
40025
41247
  ] }),
40026
41248
  /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: {
40027
41249
  display: "flex",
@@ -40859,7 +42081,9 @@ const MemoizedHeader = reactExports.memo(function MemoizedHeader2({
40859
42081
  onRedo,
40860
42082
  canUndo,
40861
42083
  canRedo,
40862
- onUnCorrectAll
42084
+ onUnCorrectAll,
42085
+ annotationsEnabled,
42086
+ onAnnotationsToggle
40863
42087
  }) {
40864
42088
  return /* @__PURE__ */ jsxRuntimeExports.jsx(
40865
42089
  Header,
@@ -40884,7 +42108,9 @@ const MemoizedHeader = reactExports.memo(function MemoizedHeader2({
40884
42108
  onRedo,
40885
42109
  canUndo,
40886
42110
  canRedo,
40887
- onUnCorrectAll
42111
+ onUnCorrectAll,
42112
+ annotationsEnabled,
42113
+ onAnnotationsToggle
40888
42114
  }
40889
42115
  );
40890
42116
  });
@@ -40920,10 +42146,14 @@ function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly,
40920
42146
  const [annotations, setAnnotations] = reactExports.useState([]);
40921
42147
  const [isAnnotationModalOpen, setIsAnnotationModalOpen] = reactExports.useState(false);
40922
42148
  const [pendingAnnotation, setPendingAnnotation] = reactExports.useState(null);
40923
- const [annotationsEnabled] = reactExports.useState(() => {
42149
+ const [annotationsEnabled, setAnnotationsEnabled] = reactExports.useState(() => {
40924
42150
  const saved = localStorage.getItem("annotationsEnabled");
40925
42151
  return saved !== null ? saved === "true" : true;
40926
42152
  });
42153
+ const handleAnnotationsToggle = reactExports.useCallback((enabled) => {
42154
+ setAnnotationsEnabled(enabled);
42155
+ localStorage.setItem("annotationsEnabled", String(enabled));
42156
+ }, []);
40927
42157
  const [correctionDetailOpen, setCorrectionDetailOpen] = reactExports.useState(false);
40928
42158
  const [selectedCorrection, setSelectedCorrection] = reactExports.useState(null);
40929
42159
  const theme2 = useTheme();
@@ -41473,7 +42703,9 @@ function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly,
41473
42703
  onRedo: handleRedo,
41474
42704
  canUndo,
41475
42705
  canRedo,
41476
- onUnCorrectAll: handleUnCorrectAll
42706
+ onUnCorrectAll: handleUnCorrectAll,
42707
+ annotationsEnabled,
42708
+ onAnnotationsToggle: handleAnnotationsToggle
41477
42709
  }
41478
42710
  ),
41479
42711
  /* @__PURE__ */ jsxRuntimeExports.jsxs(Grid, { container: true, direction: isMobile ? "column" : "row", children: [
@@ -41620,7 +42852,8 @@ function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly,
41620
42852
  onSave: handleSaveReplaceAllLyrics,
41621
42853
  onPlaySegment: handlePlaySegment,
41622
42854
  currentTime: currentAudioTime,
41623
- setModalSpacebarHandler: handleSetModalSpacebarHandler
42855
+ setModalSpacebarHandler: handleSetModalSpacebarHandler,
42856
+ existingSegments: data.corrected_segments
41624
42857
  }
41625
42858
  ),
41626
42859
  pendingAnnotation && /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -42025,7 +43258,7 @@ const theme = createTheme({
42025
43258
  spacing: (factor) => `${0.6 * factor}rem`
42026
43259
  // Further reduced from 0.8 * factor
42027
43260
  });
42028
- const version = "0.80.0";
43261
+ const version = "0.82.0";
42029
43262
  const packageJson = {
42030
43263
  version
42031
43264
  };
@@ -42036,4 +43269,4 @@ ReactDOM$1.createRoot(document.getElementById("root")).render(
42036
43269
  /* @__PURE__ */ jsxRuntimeExports.jsx(App, {})
42037
43270
  ] })
42038
43271
  );
42039
- //# sourceMappingURL=index-DdJTDWH3.js.map
43272
+ //# sourceMappingURL=index-COYImAcx.js.map