karaoke-gen 0.71.42__py3-none-any.whl → 0.75.53__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 (38) hide show
  1. karaoke_gen/__init__.py +32 -1
  2. karaoke_gen/audio_fetcher.py +1220 -67
  3. karaoke_gen/audio_processor.py +15 -3
  4. karaoke_gen/instrumental_review/server.py +154 -860
  5. karaoke_gen/instrumental_review/static/index.html +1529 -0
  6. karaoke_gen/karaoke_finalise/karaoke_finalise.py +87 -2
  7. karaoke_gen/karaoke_gen.py +131 -14
  8. karaoke_gen/lyrics_processor.py +172 -4
  9. karaoke_gen/utils/bulk_cli.py +3 -0
  10. karaoke_gen/utils/cli_args.py +7 -4
  11. karaoke_gen/utils/gen_cli.py +221 -5
  12. karaoke_gen/utils/remote_cli.py +786 -43
  13. {karaoke_gen-0.71.42.dist-info → karaoke_gen-0.75.53.dist-info}/METADATA +109 -4
  14. {karaoke_gen-0.71.42.dist-info → karaoke_gen-0.75.53.dist-info}/RECORD +37 -31
  15. lyrics_transcriber/core/controller.py +76 -2
  16. lyrics_transcriber/frontend/package.json +1 -1
  17. lyrics_transcriber/frontend/src/App.tsx +6 -4
  18. lyrics_transcriber/frontend/src/api.ts +25 -10
  19. lyrics_transcriber/frontend/src/components/Header.tsx +38 -12
  20. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +17 -3
  21. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +185 -0
  22. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +704 -0
  23. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/UpcomingWordsBar.tsx +80 -0
  24. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +905 -0
  25. lyrics_transcriber/frontend/src/components/ModeSelectionModal.tsx +127 -0
  26. lyrics_transcriber/frontend/src/components/ReplaceAllLyricsModal.tsx +190 -542
  27. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -1
  28. lyrics_transcriber/frontend/web_assets/assets/{index-DdJTDWH3.js → index-BECn1o8Q.js} +1802 -553
  29. lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js.map +1 -0
  30. lyrics_transcriber/frontend/web_assets/index.html +1 -1
  31. lyrics_transcriber/output/countdown_processor.py +39 -0
  32. lyrics_transcriber/review/server.py +5 -5
  33. lyrics_transcriber/transcribers/audioshake.py +96 -7
  34. lyrics_transcriber/types.py +14 -12
  35. lyrics_transcriber/frontend/web_assets/assets/index-DdJTDWH3.js.map +0 -1
  36. {karaoke_gen-0.71.42.dist-info → karaoke_gen-0.75.53.dist-info}/WHEEL +0 -0
  37. {karaoke_gen-0.71.42.dist-info → karaoke_gen-0.75.53.dist-info}/entry_points.txt +0 -0
  38. {karaoke_gen-0.71.42.dist-info → karaoke_gen-0.75.53.dist-info}/licenses/LICENSE +0 -0
@@ -34056,13 +34056,26 @@ function validateCorrectionData(data) {
34056
34056
  return CorrectionDataSchema.parse(data);
34057
34057
  }
34058
34058
  class LiveApiClient {
34059
- constructor(baseUrl) {
34059
+ constructor(baseUrl, reviewToken) {
34060
+ __publicField(this, "reviewToken");
34060
34061
  __publicField(this, "isUpdatingHandlers", false);
34061
34062
  this.baseUrl = baseUrl;
34062
34063
  this.baseUrl = baseUrl.replace(/\/$/, "");
34064
+ this.reviewToken = reviewToken;
34065
+ }
34066
+ /**
34067
+ * Build URL with reviewToken query parameter if available
34068
+ */
34069
+ buildUrl(path) {
34070
+ const url = `${this.baseUrl}${path}`;
34071
+ if (this.reviewToken) {
34072
+ const separator = url.includes("?") ? "&" : "?";
34073
+ return `${url}${separator}review_token=${encodeURIComponent(this.reviewToken)}`;
34074
+ }
34075
+ return url;
34063
34076
  }
34064
34077
  async getCorrectionData() {
34065
- const response = await fetch(`${this.baseUrl}/correction-data`);
34078
+ const response = await fetch(this.buildUrl("/correction-data"));
34066
34079
  if (!response.ok) {
34067
34080
  throw new Error(`API error: ${response.statusText}`);
34068
34081
  }
@@ -34079,7 +34092,7 @@ class LiveApiClient {
34079
34092
  corrections: data.corrections,
34080
34093
  corrected_segments: data.corrected_segments
34081
34094
  };
34082
- const response = await fetch(`${this.baseUrl}/complete`, {
34095
+ const response = await fetch(this.buildUrl("/complete"), {
34083
34096
  method: "POST",
34084
34097
  headers: {
34085
34098
  "Content-Type": "application/json"
@@ -34091,14 +34104,14 @@ class LiveApiClient {
34091
34104
  }
34092
34105
  }
34093
34106
  getAudioUrl(audioHash) {
34094
- return `${this.baseUrl}/audio/${audioHash}`;
34107
+ return this.buildUrl(`/audio/${audioHash}`);
34095
34108
  }
34096
34109
  async generatePreviewVideo(data) {
34097
34110
  const updatePayload = {
34098
34111
  corrections: data.corrections,
34099
34112
  corrected_segments: data.corrected_segments
34100
34113
  };
34101
- const response = await fetch(`${this.baseUrl}/preview-video`, {
34114
+ const response = await fetch(this.buildUrl("/preview-video"), {
34102
34115
  method: "POST",
34103
34116
  headers: {
34104
34117
  "Content-Type": "application/json"
@@ -34114,14 +34127,14 @@ class LiveApiClient {
34114
34127
  return await response.json();
34115
34128
  }
34116
34129
  getPreviewVideoUrl(previewHash) {
34117
- return `${this.baseUrl}/preview-video/${previewHash}`;
34130
+ return this.buildUrl(`/preview-video/${previewHash}`);
34118
34131
  }
34119
34132
  async updateHandlers(enabledHandlers) {
34120
34133
  console.log("API: Starting handler update...");
34121
34134
  this.isUpdatingHandlers = true;
34122
34135
  console.log("API: Set isUpdatingHandlers to", this.isUpdatingHandlers);
34123
34136
  try {
34124
- const response = await fetch(`${this.baseUrl}/handlers`, {
34137
+ const response = await fetch(this.buildUrl("/handlers"), {
34125
34138
  method: "POST",
34126
34139
  headers: {
34127
34140
  "Content-Type": "application/json"
@@ -34147,7 +34160,7 @@ class LiveApiClient {
34147
34160
  source,
34148
34161
  lyrics
34149
34162
  };
34150
- const response = await fetch(`${this.baseUrl}/add-lyrics`, {
34163
+ const response = await fetch(this.buildUrl("/add-lyrics"), {
34151
34164
  method: "POST",
34152
34165
  headers: {
34153
34166
  "Content-Type": "application/json"
@@ -34165,7 +34178,7 @@ class LiveApiClient {
34165
34178
  }
34166
34179
  async submitAnnotations(annotations) {
34167
34180
  for (const annotation of annotations) {
34168
- const response = await fetch(`${this.baseUrl}/v1/annotations`, {
34181
+ const response = await fetch(this.buildUrl("/v1/annotations"), {
34169
34182
  method: "POST",
34170
34183
  headers: {
34171
34184
  "Content-Type": "application/json"
@@ -34178,7 +34191,7 @@ class LiveApiClient {
34178
34191
  }
34179
34192
  }
34180
34193
  async getAnnotationStats() {
34181
- const response = await fetch(`${this.baseUrl}/v1/annotations/stats`);
34194
+ const response = await fetch(this.buildUrl("/v1/annotations/stats"));
34182
34195
  if (!response.ok) {
34183
34196
  throw new Error(`API error: ${response.statusText}`);
34184
34197
  }
@@ -36255,7 +36268,7 @@ const ZoomInIcon = createSvgIcon([/* @__PURE__ */ jsxRuntimeExports.jsx("path",
36255
36268
  const ZoomOutIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
36256
36269
  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
36270
  }), "ZoomOut");
36258
- const ArrowBack = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
36271
+ const ArrowBackIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
36259
36272
  d: "M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20z"
36260
36273
  }), "ArrowBack");
36261
36274
  const ArrowForwardIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
@@ -36614,7 +36627,7 @@ const TimelineControls = reactExports.memo(({
36614
36627
  onClick: onScrollLeft,
36615
36628
  disabled: visibleStartTime <= startTime,
36616
36629
  size: "small",
36617
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowBack, {})
36630
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowBackIcon, {})
36618
36631
  }
36619
36632
  ) }),
36620
36633
  /* @__PURE__ */ jsxRuntimeExports.jsx(Tooltip, { title: "Zoom Out (Show More Time)", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -38118,6 +38131,12 @@ function PreviewVideoSection({
38118
38131
  ) })
38119
38132
  ] });
38120
38133
  }
38134
+ const BlockIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
38135
+ 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"
38136
+ }), "Block");
38137
+ const ClearAllIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
38138
+ d: "M5 13h14v-2H5zm-2 4h14v-2H3zM7 7v2h14V7z"
38139
+ }), "ClearAll");
38121
38140
  const CloudUpload = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
38122
38141
  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
38142
  }), "CloudUpload");
@@ -38127,6 +38146,9 @@ const ContentPasteIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("pa
38127
38146
  const EditIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
38128
38147
  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
38148
  }), "Edit");
38149
+ const EditNoteIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
38150
+ 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"
38151
+ }), "EditNote");
38130
38152
  const FindReplaceIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
38131
38153
  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
38154
  }), "FindReplace");
@@ -38142,12 +38164,18 @@ const OndemandVideo = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path"
38142
38164
  const PauseIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
38143
38165
  d: "M6 19h4V5H6zm8-14v14h4V5z"
38144
38166
  }), "Pause");
38167
+ const RateReviewIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
38168
+ 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"
38169
+ }), "RateReview");
38145
38170
  const RedoIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
38146
38171
  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
38172
  }), "Redo");
38148
38173
  const RestoreIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
38149
38174
  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
38175
  }), "Restore");
38176
+ const SyncIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
38177
+ 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"
38178
+ }), "Sync");
38151
38179
  const TimerIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
38152
38180
  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
38181
  }), "Timer");
@@ -38408,7 +38436,7 @@ function ReviewChangesModal({
38408
38436
  {
38409
38437
  onClick: onClose,
38410
38438
  color: "warning",
38411
- startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowBack, {}),
38439
+ startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowBackIcon, {}),
38412
38440
  sx: { mr: "auto" },
38413
38441
  children: "Cancel"
38414
38442
  }
@@ -38428,545 +38456,1735 @@ function ReviewChangesModal({
38428
38456
  }
38429
38457
  );
38430
38458
  }
38431
- function ReplaceAllLyricsModal({
38459
+ function ModeSelectionModal({
38432
38460
  open,
38433
38461
  onClose,
38434
- onSave,
38435
- onPlaySegment,
38436
- currentTime = 0,
38437
- setModalSpacebarHandler
38462
+ onSelectReplace,
38463
+ onSelectResync,
38464
+ hasExistingLyrics
38438
38465
  }) {
38439
- 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;
38448
- }
38449
- return 600;
38450
- }, []);
38451
- const parseInfo = reactExports.useMemo(() => {
38452
- if (!inputText.trim()) return { lines: 0, words: 0 };
38453
- const lines = inputText.trim().split("\n").filter((line2) => line2.trim().length > 0);
38454
- const totalWords = lines.reduce((count, line2) => {
38455
- return count + line2.trim().split(/\s+/).length;
38456
- }, 0);
38457
- return { lines: lines.length, words: totalWords };
38458
- }, [inputText]);
38459
- const processLyrics = reactExports.useCallback(() => {
38460
- if (!inputText.trim()) return;
38461
- const lines = inputText.trim().split("\n").filter((line2) => line2.trim().length > 0);
38462
- const newSegments = [];
38463
- const allWords = [];
38464
- lines.forEach((line2) => {
38465
- 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 = {
38480
- id: nanoid(),
38481
- text: line2.trim(),
38482
- words: segmentWords,
38483
- 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
- }
38553
- });
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;
38578
- }
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]);
38608
- const handleClose = reactExports.useCallback(() => {
38609
- cleanupManualSync();
38610
- setInputText("");
38611
- setIsReplaced(false);
38612
- setGlobalSegment(null);
38613
- setOriginalSegments([]);
38614
- setCurrentSegments([]);
38615
- 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);
38645
- 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
38466
  return /* @__PURE__ */ jsxRuntimeExports.jsxs(
38713
38467
  Dialog,
38714
38468
  {
38715
38469
  open,
38716
- onClose: handleClose,
38717
- maxWidth: false,
38470
+ onClose,
38471
+ maxWidth: "sm",
38718
38472
  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
38473
  children: [
38734
38474
  /* @__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, {}) })
38475
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { sx: { flex: 1 }, children: "Edit All Lyrics" }),
38476
+ /* @__PURE__ */ jsxRuntimeExports.jsx(IconButton, { onClick: onClose, sx: { ml: "auto" }, children: /* @__PURE__ */ jsxRuntimeExports.jsx(CloseIcon, {}) })
38737
38477
  ] }),
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: [
38751
- /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "h6", gutterBottom: true, children: "Paste your new lyrics below:" }),
38752
- /* @__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
- /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { display: "flex", gap: 2, mb: 2 }, children: [
38754
- /* @__PURE__ */ jsxRuntimeExports.jsx(
38755
- Button,
38756
- {
38757
- variant: "outlined",
38758
- onClick: handlePasteFromClipboard,
38759
- startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(ContentPasteIcon, {}),
38760
- size: "small",
38761
- children: "Paste from Clipboard"
38762
- }
38763
- ),
38764
- /* @__PURE__ */ jsxRuntimeExports.jsxs(Typography, { variant: "body2", sx: {
38765
- alignSelf: "center",
38766
- color: "text.secondary",
38767
- fontWeight: "medium"
38768
- }, children: [
38769
- parseInfo.lines,
38770
- " lines, ",
38771
- parseInfo.words,
38772
- " words"
38773
- ] })
38774
- ] }),
38775
- /* @__PURE__ */ jsxRuntimeExports.jsx(
38776
- TextField,
38777
- {
38778
- multiline: true,
38779
- rows: 15,
38780
- value: inputText,
38781
- onChange: (e) => setInputText(e.target.value),
38782
- placeholder: "Paste your lyrics here...\nEach line will become a segment\nWords will be separated by spaces",
38783
- sx: {
38784
- flexGrow: 1,
38785
- "& .MuiInputBase-root": {
38786
- height: "100%",
38787
- alignItems: "flex-start"
38788
- }
38789
- }
38478
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(DialogContent, { dividers: true, children: [
38479
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "body1", sx: { mb: 3 }, children: "Choose how you want to edit the lyrics:" }),
38480
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { display: "flex", flexDirection: "column", gap: 2 }, children: [
38481
+ hasExistingLyrics && /* @__PURE__ */ jsxRuntimeExports.jsx(
38482
+ Paper,
38483
+ {
38484
+ sx: {
38485
+ p: 2,
38486
+ cursor: "pointer",
38487
+ border: 2,
38488
+ borderColor: "primary.main",
38489
+ "&:hover": {
38490
+ bgcolor: "action.hover",
38491
+ borderColor: "primary.dark"
38790
38492
  }
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"
38493
+ },
38494
+ onClick: onSelectResync,
38495
+ children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { display: "flex", alignItems: "flex-start", gap: 2 }, children: [
38496
+ /* @__PURE__ */ jsxRuntimeExports.jsx(SyncIcon, { color: "primary", sx: { fontSize: 40, mt: 0.5 } }),
38497
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { children: [
38498
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "h6", color: "primary", children: "Re-sync Existing Lyrics" }),
38499
+ /* @__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." }),
38500
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "caption", color: "success.main", sx: { mt: 1, display: "block" }, children: "Recommended for fixing timing drift" })
38501
+ ] })
38502
+ ] })
38503
+ }
38504
+ ),
38505
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
38506
+ Paper,
38507
+ {
38508
+ sx: {
38509
+ p: 2,
38510
+ cursor: "pointer",
38511
+ border: 1,
38512
+ borderColor: "divider",
38513
+ "&:hover": {
38514
+ bgcolor: "action.hover",
38515
+ borderColor: "text.secondary"
38800
38516
  }
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."
38517
+ },
38518
+ onClick: onSelectReplace,
38519
+ children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { display: "flex", alignItems: "flex-start", gap: 2 }, children: [
38520
+ /* @__PURE__ */ jsxRuntimeExports.jsx(ContentPasteIcon, { sx: { fontSize: 40, mt: 0.5, color: "text.secondary" } }),
38521
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { children: [
38522
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "h6", children: "Replace All Lyrics" }),
38523
+ /* @__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." }),
38524
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "caption", color: "warning.main", sx: { mt: 1, display: "block" }, children: "All existing timing data will be lost" })
38814
38525
  ] })
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
38526
  ] })
38858
- ] })
38527
+ }
38859
38528
  )
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
- ) })
38529
+ ] })
38530
+ ] }),
38531
+ /* @__PURE__ */ jsxRuntimeExports.jsx(DialogActions, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { onClick: onClose, color: "inherit", children: "Cancel" }) })
38872
38532
  ]
38873
38533
  }
38874
38534
  );
38875
38535
  }
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
- ] })
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)
38536
+ const TIME_BAR_HEIGHT = 28;
38537
+ const WORD_BLOCK_HEIGHT = 24;
38538
+ const WORD_LEVEL_SPACING = 50;
38539
+ const CANVAS_PADDING = 8;
38540
+ const TEXT_ABOVE_BLOCK = 14;
38541
+ const RESIZE_HANDLE_SIZE = 8;
38542
+ const RESIZE_HANDLE_HITAREA = 12;
38543
+ const PLAYHEAD_COLOR = "#ffffff";
38544
+ const WORD_BLOCK_COLOR = "#d32f2f";
38545
+ const WORD_BLOCK_SELECTED_COLOR = "#b71c1c";
38546
+ const WORD_BLOCK_CURRENT_COLOR = "#f44336";
38547
+ const WORD_TEXT_CURRENT_COLOR = "#d32f2f";
38548
+ const UPCOMING_WORD_BG = "#fff9c4";
38549
+ const UPCOMING_WORD_TEXT = "#000000";
38550
+ const TIME_BAR_BG = "#f5f5f5";
38551
+ const TIME_BAR_TEXT = "#666666";
38552
+ const TIMELINE_BG = "#e0e0e0";
38553
+ function buildWordToSegmentMap(segments) {
38554
+ const map = /* @__PURE__ */ new Map();
38555
+ segments.forEach((segment, idx) => {
38556
+ segment.words.forEach((word) => {
38557
+ map.set(word.id, idx);
38558
+ });
38559
+ });
38560
+ return map;
38561
+ }
38562
+ function calculateWordLevels(words, segments) {
38563
+ const levels = /* @__PURE__ */ new Map();
38564
+ const wordToSegment = buildWordToSegmentMap(segments);
38565
+ const segmentsWithTiming = segments.map((segment, idx) => {
38566
+ const timedWords = segment.words.filter((w) => w.start_time !== null);
38567
+ const minStart = timedWords.length > 0 ? Math.min(...timedWords.map((w) => w.start_time)) : Infinity;
38568
+ return { idx, minStart };
38569
+ }).filter((s) => s.minStart !== Infinity).sort((a, b) => a.minStart - b.minStart);
38570
+ const segmentLevels = /* @__PURE__ */ new Map();
38571
+ segmentsWithTiming.forEach(({ idx }, orderIndex) => {
38572
+ segmentLevels.set(idx, orderIndex % 2);
38573
+ });
38574
+ for (const word of words) {
38575
+ const segmentIdx = wordToSegment.get(word.id);
38576
+ if (segmentIdx !== void 0 && segmentLevels.has(segmentIdx)) {
38577
+ levels.set(word.id, segmentLevels.get(segmentIdx));
38578
+ } else {
38579
+ levels.set(word.id, 0);
38580
+ }
38581
+ }
38582
+ return levels;
38583
+ }
38584
+ function formatTime(seconds) {
38585
+ const mins = Math.floor(seconds / 60);
38586
+ const secs = Math.floor(seconds % 60);
38587
+ return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
38588
+ }
38589
+ const TimelineCanvas = reactExports.memo(function TimelineCanvas2({
38590
+ words,
38591
+ segments,
38592
+ visibleStartTime,
38593
+ visibleEndTime,
38594
+ currentTime,
38595
+ selectedWordIds,
38596
+ onWordClick,
38597
+ onBackgroundClick,
38598
+ onTimeBarClick,
38599
+ onSelectionComplete,
38600
+ onWordTimingChange,
38601
+ onWordsMove,
38602
+ syncWordIndex,
38603
+ isManualSyncing,
38604
+ onScrollChange,
38605
+ audioDuration,
38606
+ zoomSeconds,
38607
+ height: height2 = 200
38608
+ }) {
38609
+ const canvasRef = reactExports.useRef(null);
38610
+ const containerRef = reactExports.useRef(null);
38611
+ const [canvasWidth, setCanvasWidth] = reactExports.useState(800);
38612
+ const animationFrameRef = reactExports.useRef();
38613
+ const wordLevelsRef = reactExports.useRef(/* @__PURE__ */ new Map());
38614
+ const [dragMode, setDragMode] = reactExports.useState("none");
38615
+ const dragStartRef = reactExports.useRef(null);
38616
+ const dragWordIdRef = reactExports.useRef(null);
38617
+ const dragOriginalTimesRef = reactExports.useRef(/* @__PURE__ */ new Map());
38618
+ const [selectionRect, setSelectionRect] = reactExports.useState(null);
38619
+ const [hoveredWordId, setHoveredWordId] = reactExports.useState(null);
38620
+ const [cursorStyle, setCursorStyle] = reactExports.useState("default");
38621
+ reactExports.useEffect(() => {
38622
+ const updateWidth = () => {
38623
+ if (containerRef.current) {
38624
+ setCanvasWidth(containerRef.current.clientWidth);
38625
+ }
38626
+ };
38627
+ updateWidth();
38628
+ const resizeObserver = new ResizeObserver(updateWidth);
38629
+ if (containerRef.current) {
38630
+ resizeObserver.observe(containerRef.current);
38631
+ }
38632
+ return () => resizeObserver.disconnect();
38633
+ }, []);
38634
+ reactExports.useEffect(() => {
38635
+ wordLevelsRef.current = calculateWordLevels(words, segments);
38636
+ }, [words, segments]);
38637
+ const timeToX = reactExports.useCallback((time) => {
38638
+ const duration2 = visibleEndTime - visibleStartTime;
38639
+ if (duration2 <= 0) return 0;
38640
+ return CANVAS_PADDING + (time - visibleStartTime) / duration2 * (canvasWidth - CANVAS_PADDING * 2);
38641
+ }, [visibleStartTime, visibleEndTime, canvasWidth]);
38642
+ const xToTime = reactExports.useCallback((x) => {
38643
+ const duration2 = visibleEndTime - visibleStartTime;
38644
+ return visibleStartTime + (x - CANVAS_PADDING) / (canvasWidth - CANVAS_PADDING * 2) * duration2;
38645
+ }, [visibleStartTime, visibleEndTime, canvasWidth]);
38646
+ const getWordBounds = reactExports.useCallback((word) => {
38647
+ if (word.start_time === null || word.end_time === null) return null;
38648
+ const level = wordLevelsRef.current.get(word.id) || 0;
38649
+ const startX = timeToX(word.start_time);
38650
+ const endX = timeToX(word.end_time);
38651
+ const blockWidth = Math.max(endX - startX, 4);
38652
+ const y = TIME_BAR_HEIGHT + CANVAS_PADDING + TEXT_ABOVE_BLOCK + level * WORD_LEVEL_SPACING;
38653
+ return { startX, endX, blockWidth, y, level };
38654
+ }, [timeToX]);
38655
+ const isNearResizeHandlePos = reactExports.useCallback((word, x, y) => {
38656
+ const bounds = getWordBounds(word);
38657
+ if (!bounds) return false;
38658
+ const handleX = bounds.startX + bounds.blockWidth - RESIZE_HANDLE_SIZE / 2;
38659
+ const handleY = bounds.y + WORD_BLOCK_HEIGHT / 2;
38660
+ return Math.abs(x - handleX) < RESIZE_HANDLE_HITAREA / 2 && Math.abs(y - handleY) < RESIZE_HANDLE_HITAREA / 2;
38661
+ }, [getWordBounds]);
38662
+ const findWordAtPosition = reactExports.useCallback((x, y) => {
38663
+ for (const word of words) {
38664
+ const bounds = getWordBounds(word);
38665
+ if (!bounds) continue;
38666
+ if (x >= bounds.startX && x <= bounds.startX + bounds.blockWidth && y >= bounds.y && y <= bounds.y + WORD_BLOCK_HEIGHT) {
38667
+ return word;
38668
+ }
38669
+ }
38670
+ return null;
38671
+ }, [words, getWordBounds]);
38672
+ const findWordsInRect = reactExports.useCallback((rect) => {
38673
+ const rectLeft = Math.min(rect.startX, rect.endX);
38674
+ const rectRight = Math.max(rect.startX, rect.endX);
38675
+ const rectTop = Math.min(rect.startY, rect.endY);
38676
+ const rectBottom = Math.max(rect.startY, rect.endY);
38677
+ const selectedIds = [];
38678
+ for (const word of words) {
38679
+ const bounds = getWordBounds(word);
38680
+ if (!bounds) continue;
38681
+ if (bounds.startX + bounds.blockWidth >= rectLeft && bounds.startX <= rectRight && bounds.y + WORD_BLOCK_HEIGHT >= rectTop && bounds.y <= rectBottom) {
38682
+ selectedIds.push(word.id);
38683
+ }
38684
+ }
38685
+ return selectedIds;
38686
+ }, [words, getWordBounds]);
38687
+ const draw = reactExports.useCallback(() => {
38688
+ var _a;
38689
+ const canvas = canvasRef.current;
38690
+ if (!canvas) return;
38691
+ const ctx = canvas.getContext("2d");
38692
+ if (!ctx) return;
38693
+ const dpr = window.devicePixelRatio || 1;
38694
+ canvas.width = canvasWidth * dpr;
38695
+ canvas.height = height2 * dpr;
38696
+ ctx.scale(dpr, dpr);
38697
+ ctx.fillStyle = TIMELINE_BG;
38698
+ ctx.fillRect(0, 0, canvasWidth, height2);
38699
+ ctx.fillStyle = TIME_BAR_BG;
38700
+ ctx.fillRect(0, 0, canvasWidth, TIME_BAR_HEIGHT);
38701
+ const duration2 = visibleEndTime - visibleStartTime;
38702
+ const secondsPerTick = duration2 > 15 ? 2 : duration2 > 8 ? 1 : 0.5;
38703
+ const startSecond = Math.ceil(visibleStartTime / secondsPerTick) * secondsPerTick;
38704
+ ctx.fillStyle = TIME_BAR_TEXT;
38705
+ ctx.font = "11px system-ui, -apple-system, sans-serif";
38706
+ ctx.textAlign = "center";
38707
+ for (let t = startSecond; t <= visibleEndTime; t += secondsPerTick) {
38708
+ const x = timeToX(t);
38709
+ ctx.beginPath();
38710
+ ctx.strokeStyle = "#999999";
38711
+ ctx.lineWidth = 1;
38712
+ ctx.moveTo(x, TIME_BAR_HEIGHT - 6);
38713
+ ctx.lineTo(x, TIME_BAR_HEIGHT);
38714
+ ctx.stroke();
38715
+ if (t % 1 === 0) {
38716
+ ctx.fillText(formatTime(t), x, TIME_BAR_HEIGHT - 10);
38717
+ }
38718
+ }
38719
+ ctx.beginPath();
38720
+ ctx.strokeStyle = "#cccccc";
38721
+ ctx.lineWidth = 1;
38722
+ ctx.moveTo(0, TIME_BAR_HEIGHT);
38723
+ ctx.lineTo(canvasWidth, TIME_BAR_HEIGHT);
38724
+ ctx.stroke();
38725
+ const wordToSegment = buildWordToSegmentMap(segments);
38726
+ const syncedWords = words.filter((w) => w.start_time !== null && w.end_time !== null);
38727
+ const currentWordId = ((_a = syncedWords.find(
38728
+ (w) => currentTime >= w.start_time && currentTime <= w.end_time
38729
+ )) == null ? void 0 : _a.id) || null;
38730
+ for (const word of syncedWords) {
38731
+ const bounds = getWordBounds(word);
38732
+ if (!bounds) continue;
38733
+ const isSelected = selectedWordIds.has(word.id);
38734
+ const isCurrent = word.id === currentWordId;
38735
+ const isHovered = word.id === hoveredWordId;
38736
+ if (isSelected) {
38737
+ ctx.fillStyle = WORD_BLOCK_SELECTED_COLOR;
38738
+ } else if (isCurrent) {
38739
+ ctx.fillStyle = WORD_BLOCK_CURRENT_COLOR;
38740
+ } else {
38741
+ ctx.fillStyle = WORD_BLOCK_COLOR;
38742
+ }
38743
+ ctx.fillRect(bounds.startX, bounds.y, bounds.blockWidth, WORD_BLOCK_HEIGHT);
38744
+ if (isSelected) {
38745
+ ctx.strokeStyle = "#ffffff";
38746
+ ctx.lineWidth = 2;
38747
+ ctx.strokeRect(bounds.startX, bounds.y, bounds.blockWidth, WORD_BLOCK_HEIGHT);
38748
+ if (isHovered || selectedWordIds.size === 1) {
38749
+ const handleX = bounds.startX + bounds.blockWidth - RESIZE_HANDLE_SIZE / 2;
38750
+ const handleY = bounds.y + WORD_BLOCK_HEIGHT / 2;
38751
+ ctx.beginPath();
38752
+ ctx.fillStyle = "#ffffff";
38753
+ ctx.arc(handleX, handleY, RESIZE_HANDLE_SIZE / 2, 0, Math.PI * 2);
38754
+ ctx.fill();
38755
+ ctx.strokeStyle = "#666666";
38756
+ ctx.lineWidth = 1;
38757
+ ctx.stroke();
38758
+ }
38759
+ }
38760
+ }
38761
+ const wordsBySegment = /* @__PURE__ */ new Map();
38762
+ for (const word of syncedWords) {
38763
+ const segIdx = wordToSegment.get(word.id);
38764
+ if (segIdx !== void 0) {
38765
+ if (!wordsBySegment.has(segIdx)) {
38766
+ wordsBySegment.set(segIdx, []);
38767
+ }
38768
+ wordsBySegment.get(segIdx).push(word);
38769
+ }
38770
+ }
38771
+ ctx.font = "11px system-ui, -apple-system, sans-serif";
38772
+ ctx.textAlign = "left";
38773
+ for (const [, segmentWords] of wordsBySegment) {
38774
+ const sortedWords = [...segmentWords].sort(
38775
+ (a, b) => (a.start_time || 0) - (b.start_time || 0)
38957
38776
  );
38958
- return /* @__PURE__ */ jsxRuntimeExports.jsx(
38959
- SegmentProgressItem,
38777
+ if (sortedWords.length === 0) continue;
38778
+ const level = wordLevelsRef.current.get(sortedWords[0].id) || 0;
38779
+ const textY = TIME_BAR_HEIGHT + CANVAS_PADDING + TEXT_ABOVE_BLOCK + level * WORD_LEVEL_SPACING - 3;
38780
+ let rightmostTextEnd = -Infinity;
38781
+ for (const word of sortedWords) {
38782
+ const blockStartX = timeToX(word.start_time);
38783
+ const textWidth = ctx.measureText(word.text).width;
38784
+ const textStartX = Math.max(blockStartX, rightmostTextEnd + 3);
38785
+ if (textStartX < canvasWidth - 10) {
38786
+ const isCurrent = word.id === currentWordId;
38787
+ ctx.fillStyle = isCurrent ? WORD_TEXT_CURRENT_COLOR : "#333333";
38788
+ ctx.fillText(word.text, textStartX, textY);
38789
+ rightmostTextEnd = textStartX + textWidth;
38790
+ }
38791
+ }
38792
+ }
38793
+ if (isManualSyncing && syncWordIndex >= 0) {
38794
+ const upcomingWords = words.slice(syncWordIndex).filter((w) => w.start_time === null);
38795
+ const playheadX = timeToX(currentTime);
38796
+ let offsetX = playheadX + 10;
38797
+ ctx.font = "11px system-ui, -apple-system, sans-serif";
38798
+ for (let i = 0; i < Math.min(upcomingWords.length, 12); i++) {
38799
+ const word = upcomingWords[i];
38800
+ const textWidth = ctx.measureText(word.text).width + 10;
38801
+ ctx.fillStyle = UPCOMING_WORD_BG;
38802
+ ctx.fillRect(offsetX, TIME_BAR_HEIGHT + CANVAS_PADDING + WORD_LEVEL_SPACING + 60, textWidth, 20);
38803
+ ctx.fillStyle = UPCOMING_WORD_TEXT;
38804
+ ctx.textAlign = "left";
38805
+ ctx.fillText(word.text, offsetX + 5, TIME_BAR_HEIGHT + CANVAS_PADDING + WORD_LEVEL_SPACING + 74);
38806
+ offsetX += textWidth + 3;
38807
+ if (offsetX > canvasWidth - 20) break;
38808
+ }
38809
+ }
38810
+ if (currentTime >= visibleStartTime && currentTime <= visibleEndTime) {
38811
+ const playheadX = timeToX(currentTime);
38812
+ ctx.beginPath();
38813
+ ctx.fillStyle = PLAYHEAD_COLOR;
38814
+ ctx.strokeStyle = "#333333";
38815
+ ctx.lineWidth = 1;
38816
+ ctx.moveTo(playheadX - 6, 2);
38817
+ ctx.lineTo(playheadX + 6, 2);
38818
+ ctx.lineTo(playheadX, TIME_BAR_HEIGHT - 4);
38819
+ ctx.closePath();
38820
+ ctx.fill();
38821
+ ctx.stroke();
38822
+ ctx.beginPath();
38823
+ ctx.strokeStyle = PLAYHEAD_COLOR;
38824
+ ctx.lineWidth = 2;
38825
+ ctx.moveTo(playheadX, TIME_BAR_HEIGHT);
38826
+ ctx.lineTo(playheadX, height2);
38827
+ ctx.stroke();
38828
+ ctx.beginPath();
38829
+ ctx.strokeStyle = "rgba(0,0,0,0.4)";
38830
+ ctx.lineWidth = 1;
38831
+ ctx.moveTo(playheadX + 1, TIME_BAR_HEIGHT);
38832
+ ctx.lineTo(playheadX + 1, height2);
38833
+ ctx.stroke();
38834
+ }
38835
+ if (selectionRect) {
38836
+ ctx.fillStyle = "rgba(25, 118, 210, 0.2)";
38837
+ ctx.strokeStyle = "rgba(25, 118, 210, 0.8)";
38838
+ ctx.lineWidth = 1;
38839
+ const rectX = Math.min(selectionRect.startX, selectionRect.endX);
38840
+ const rectY = Math.min(selectionRect.startY, selectionRect.endY);
38841
+ const rectW = Math.abs(selectionRect.endX - selectionRect.startX);
38842
+ const rectH = Math.abs(selectionRect.endY - selectionRect.startY);
38843
+ ctx.fillRect(rectX, rectY, rectW, rectH);
38844
+ ctx.strokeRect(rectX, rectY, rectW, rectH);
38845
+ }
38846
+ }, [
38847
+ canvasWidth,
38848
+ height2,
38849
+ visibleStartTime,
38850
+ visibleEndTime,
38851
+ currentTime,
38852
+ words,
38853
+ segments,
38854
+ selectedWordIds,
38855
+ selectionRect,
38856
+ hoveredWordId,
38857
+ syncWordIndex,
38858
+ isManualSyncing,
38859
+ timeToX,
38860
+ getWordBounds
38861
+ ]);
38862
+ reactExports.useEffect(() => {
38863
+ const animate = () => {
38864
+ draw();
38865
+ animationFrameRef.current = requestAnimationFrame(animate);
38866
+ };
38867
+ animate();
38868
+ return () => {
38869
+ if (animationFrameRef.current) {
38870
+ cancelAnimationFrame(animationFrameRef.current);
38871
+ }
38872
+ };
38873
+ }, [draw]);
38874
+ const handleMouseDown = reactExports.useCallback((e) => {
38875
+ var _a;
38876
+ const rect = (_a = canvasRef.current) == null ? void 0 : _a.getBoundingClientRect();
38877
+ if (!rect) return;
38878
+ const x = e.clientX - rect.left;
38879
+ const y = e.clientY - rect.top;
38880
+ const time = xToTime(x);
38881
+ if (y < TIME_BAR_HEIGHT) {
38882
+ onTimeBarClick(Math.max(0, time));
38883
+ return;
38884
+ }
38885
+ const clickedWord = findWordAtPosition(x, y);
38886
+ if (clickedWord && selectedWordIds.has(clickedWord.id)) {
38887
+ if (isNearResizeHandlePos(clickedWord, x, y)) {
38888
+ setDragMode("resize");
38889
+ dragStartRef.current = { x, y, time };
38890
+ dragWordIdRef.current = clickedWord.id;
38891
+ dragOriginalTimesRef.current = /* @__PURE__ */ new Map([[clickedWord.id, {
38892
+ start: clickedWord.start_time,
38893
+ end: clickedWord.end_time
38894
+ }]]);
38895
+ return;
38896
+ }
38897
+ setDragMode("move");
38898
+ dragStartRef.current = { x, y, time };
38899
+ dragWordIdRef.current = clickedWord.id;
38900
+ const originalTimes = /* @__PURE__ */ new Map();
38901
+ for (const wordId of selectedWordIds) {
38902
+ const word = words.find((w) => w.id === wordId);
38903
+ if (word && word.start_time !== null && word.end_time !== null) {
38904
+ originalTimes.set(wordId, { start: word.start_time, end: word.end_time });
38905
+ }
38906
+ }
38907
+ dragOriginalTimesRef.current = originalTimes;
38908
+ return;
38909
+ }
38910
+ if (clickedWord) {
38911
+ onWordClick(clickedWord.id, e);
38912
+ return;
38913
+ }
38914
+ setDragMode("selection");
38915
+ dragStartRef.current = { x, y, time };
38916
+ setSelectionRect({ startX: x, startY: y, endX: x, endY: y });
38917
+ }, [xToTime, onTimeBarClick, findWordAtPosition, selectedWordIds, isNearResizeHandlePos, onWordClick, words]);
38918
+ const handleMouseMove = reactExports.useCallback((e) => {
38919
+ var _a;
38920
+ const rect = (_a = canvasRef.current) == null ? void 0 : _a.getBoundingClientRect();
38921
+ if (!rect) return;
38922
+ const x = e.clientX - rect.left;
38923
+ const y = e.clientY - rect.top;
38924
+ const time = xToTime(x);
38925
+ if (dragMode === "none") {
38926
+ const hoveredWord = findWordAtPosition(x, y);
38927
+ setHoveredWordId((hoveredWord == null ? void 0 : hoveredWord.id) || null);
38928
+ if (hoveredWord && selectedWordIds.has(hoveredWord.id)) {
38929
+ const nearHandle = isNearResizeHandlePos(hoveredWord, x, y);
38930
+ setCursorStyle(nearHandle ? "ew-resize" : "grab");
38931
+ } else if (hoveredWord) {
38932
+ setCursorStyle("pointer");
38933
+ } else if (y < TIME_BAR_HEIGHT) {
38934
+ setCursorStyle("pointer");
38935
+ } else {
38936
+ setCursorStyle("default");
38937
+ }
38938
+ }
38939
+ if (!dragStartRef.current) return;
38940
+ if (dragMode === "selection") {
38941
+ setSelectionRect({
38942
+ startX: dragStartRef.current.x,
38943
+ startY: dragStartRef.current.y,
38944
+ endX: x,
38945
+ endY: y
38946
+ });
38947
+ } else if (dragMode === "resize" && dragWordIdRef.current) {
38948
+ const originalTimes = dragOriginalTimesRef.current.get(dragWordIdRef.current);
38949
+ if (originalTimes) {
38950
+ const deltaTime = time - dragStartRef.current.time;
38951
+ const newEndTime = Math.max(originalTimes.start + 0.05, originalTimes.end + deltaTime);
38952
+ onWordTimingChange(dragWordIdRef.current, originalTimes.start, newEndTime);
38953
+ }
38954
+ setCursorStyle("ew-resize");
38955
+ } else if (dragMode === "move") {
38956
+ const deltaTime = time - dragStartRef.current.time;
38957
+ const updates = [];
38958
+ for (const [wordId, originalTimes] of dragOriginalTimesRef.current) {
38959
+ const newStartTime = Math.max(0, originalTimes.start + deltaTime);
38960
+ const newEndTime = Math.max(newStartTime + 0.05, originalTimes.end + deltaTime);
38961
+ updates.push({
38962
+ wordId,
38963
+ newStartTime,
38964
+ newEndTime
38965
+ });
38966
+ }
38967
+ if (updates.length > 0) {
38968
+ onWordsMove(updates);
38969
+ }
38970
+ setCursorStyle("grabbing");
38971
+ }
38972
+ }, [dragMode, xToTime, findWordAtPosition, selectedWordIds, isNearResizeHandlePos, onWordTimingChange, onWordsMove]);
38973
+ const handleMouseUp = reactExports.useCallback((e) => {
38974
+ var _a;
38975
+ const rect = (_a = canvasRef.current) == null ? void 0 : _a.getBoundingClientRect();
38976
+ if (dragMode === "selection" && dragStartRef.current && rect) {
38977
+ const endX = e.clientX - rect.left;
38978
+ const endY = e.clientY - rect.top;
38979
+ const dragDistance = Math.sqrt(
38980
+ Math.pow(endX - dragStartRef.current.x, 2) + Math.pow(endY - dragStartRef.current.y, 2)
38981
+ );
38982
+ if (dragDistance < 5) {
38983
+ onBackgroundClick();
38984
+ } else {
38985
+ const finalRect = {
38986
+ startX: dragStartRef.current.x,
38987
+ startY: dragStartRef.current.y,
38988
+ endX,
38989
+ endY
38990
+ };
38991
+ const selectedIds = findWordsInRect(finalRect);
38992
+ if (selectedIds.length > 0) {
38993
+ onSelectionComplete(selectedIds);
38994
+ }
38995
+ }
38996
+ }
38997
+ setDragMode("none");
38998
+ dragStartRef.current = null;
38999
+ dragWordIdRef.current = null;
39000
+ dragOriginalTimesRef.current = /* @__PURE__ */ new Map();
39001
+ setSelectionRect(null);
39002
+ setCursorStyle("default");
39003
+ }, [dragMode, onBackgroundClick, findWordsInRect, onSelectionComplete]);
39004
+ const handleWheel = reactExports.useCallback((e) => {
39005
+ const delta = e.deltaX !== 0 ? e.deltaX : e.deltaY;
39006
+ const scrollAmount = delta / 100 * (zoomSeconds / 4);
39007
+ let newStart = Math.max(0, Math.min(audioDuration - zoomSeconds, visibleStartTime + scrollAmount));
39008
+ if (newStart !== visibleStartTime) {
39009
+ onScrollChange(newStart);
39010
+ }
39011
+ }, [visibleStartTime, zoomSeconds, audioDuration, onScrollChange]);
39012
+ const handleScrollLeft = reactExports.useCallback(() => {
39013
+ const newStart = Math.max(0, visibleStartTime - zoomSeconds * 0.25);
39014
+ onScrollChange(newStart);
39015
+ }, [visibleStartTime, zoomSeconds, onScrollChange]);
39016
+ const handleScrollRight = reactExports.useCallback(() => {
39017
+ const newStart = Math.min(audioDuration - zoomSeconds, visibleStartTime + zoomSeconds * 0.25);
39018
+ onScrollChange(Math.max(0, newStart));
39019
+ }, [visibleStartTime, zoomSeconds, audioDuration, onScrollChange]);
39020
+ 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: [
39021
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Tooltip, { title: "Scroll Left", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
39022
+ IconButton,
39023
+ {
39024
+ size: "small",
39025
+ onClick: handleScrollLeft,
39026
+ disabled: visibleStartTime <= 0,
39027
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowBackIcon, { fontSize: "small" })
39028
+ }
39029
+ ) }),
39030
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
39031
+ Box,
39032
+ {
39033
+ ref: containerRef,
39034
+ sx: {
39035
+ flexGrow: 1,
39036
+ height: height2,
39037
+ cursor: cursorStyle,
39038
+ borderRadius: 1,
39039
+ overflow: "hidden"
39040
+ },
39041
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(
39042
+ "canvas",
39043
+ {
39044
+ ref: canvasRef,
39045
+ style: {
39046
+ width: "100%",
39047
+ height: "100%",
39048
+ display: "block",
39049
+ cursor: cursorStyle
39050
+ },
39051
+ onMouseDown: handleMouseDown,
39052
+ onMouseMove: handleMouseMove,
39053
+ onMouseUp: handleMouseUp,
39054
+ onMouseLeave: handleMouseUp,
39055
+ onWheel: handleWheel
39056
+ }
39057
+ )
39058
+ }
39059
+ ),
39060
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Tooltip, { title: "Scroll Right", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
39061
+ IconButton,
39062
+ {
39063
+ size: "small",
39064
+ onClick: handleScrollRight,
39065
+ disabled: visibleStartTime >= audioDuration - zoomSeconds,
39066
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowForwardIcon, { fontSize: "small" })
39067
+ }
39068
+ ) })
39069
+ ] }) });
39070
+ });
39071
+ const UpcomingWordsBar = reactExports.memo(function UpcomingWordsBar2({
39072
+ words,
39073
+ syncWordIndex,
39074
+ isManualSyncing,
39075
+ maxWordsToShow = 20
39076
+ }) {
39077
+ const upcomingWords = reactExports.useMemo(() => {
39078
+ if (!isManualSyncing || syncWordIndex < 0) return [];
39079
+ return words.slice(syncWordIndex).filter((w) => w.start_time === null).slice(0, maxWordsToShow);
39080
+ }, [words, syncWordIndex, isManualSyncing, maxWordsToShow]);
39081
+ const totalRemaining = reactExports.useMemo(() => {
39082
+ if (!isManualSyncing || syncWordIndex < 0) return 0;
39083
+ return words.slice(syncWordIndex).filter((w) => w.start_time === null).length;
39084
+ }, [words, syncWordIndex, isManualSyncing]);
39085
+ if (upcomingWords.length === 0) {
39086
+ return null;
39087
+ }
39088
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: {
39089
+ height: 44,
39090
+ bgcolor: "grey.100",
39091
+ borderRadius: 1,
39092
+ display: "flex",
39093
+ alignItems: "center",
39094
+ px: 1,
39095
+ gap: 0.5,
39096
+ overflow: "hidden",
39097
+ boxSizing: "border-box"
39098
+ }, children: [
39099
+ upcomingWords.map((word, index) => /* @__PURE__ */ jsxRuntimeExports.jsx(
39100
+ Box,
39101
+ {
39102
+ sx: {
39103
+ px: 1,
39104
+ py: 0.5,
39105
+ borderRadius: 0.5,
39106
+ bgcolor: index === 0 ? "error.main" : "grey.300",
39107
+ color: index === 0 ? "white" : "text.primary",
39108
+ fontWeight: index === 0 ? "bold" : "normal",
39109
+ fontSize: "13px",
39110
+ fontFamily: "system-ui, -apple-system, sans-serif",
39111
+ whiteSpace: "nowrap",
39112
+ border: index === 0 ? "2px solid" : "none",
39113
+ borderColor: "error.dark"
39114
+ },
39115
+ children: word.text
39116
+ },
39117
+ word.id
39118
+ )),
39119
+ totalRemaining > maxWordsToShow && /* @__PURE__ */ jsxRuntimeExports.jsxs(Typography, { variant: "caption", color: "text.secondary", sx: { ml: 1 }, children: [
39120
+ "+",
39121
+ totalRemaining - maxWordsToShow,
39122
+ " more"
39123
+ ] })
39124
+ ] });
39125
+ });
39126
+ const SyncControls = reactExports.memo(function SyncControls2({
39127
+ isManualSyncing,
39128
+ isPaused,
39129
+ onStartSync,
39130
+ onPauseSync,
39131
+ onResumeSync,
39132
+ onClearSync,
39133
+ onEditLyrics,
39134
+ onPlay,
39135
+ onStop,
39136
+ isPlaying,
39137
+ hasSelectedWords,
39138
+ selectedWordCount,
39139
+ onUnsyncFromCursor,
39140
+ onEditSelectedWord,
39141
+ onDeleteSelected,
39142
+ canUnsyncFromCursor
39143
+ }) {
39144
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { display: "flex", flexDirection: "column", gap: 1.5 }, children: [
39145
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", flexWrap: "wrap", children: [
39146
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
39147
+ Button,
38960
39148
  {
38961
- segment,
38962
- index,
38963
- isActive
39149
+ variant: "outlined",
39150
+ color: "primary",
39151
+ onClick: onPlay,
39152
+ startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(PlayArrowIcon, {}),
39153
+ size: "small",
39154
+ disabled: isPlaying,
39155
+ children: "Play"
39156
+ }
39157
+ ),
39158
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
39159
+ Button,
39160
+ {
39161
+ variant: "outlined",
39162
+ color: "error",
39163
+ onClick: onStop,
39164
+ startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(StopIcon, {}),
39165
+ size: "small",
39166
+ disabled: !isPlaying,
39167
+ children: "Stop"
39168
+ }
39169
+ ),
39170
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Divider, { orientation: "vertical", flexItem: true, sx: { mx: 0.5 } }),
39171
+ isManualSyncing ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
39172
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
39173
+ Button,
39174
+ {
39175
+ variant: "contained",
39176
+ color: "error",
39177
+ onClick: onStartSync,
39178
+ startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(StopIcon, {}),
39179
+ size: "small",
39180
+ children: "Stop Sync"
39181
+ }
39182
+ ),
39183
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
39184
+ Button,
39185
+ {
39186
+ variant: "outlined",
39187
+ color: isPaused ? "success" : "warning",
39188
+ onClick: isPaused ? onResumeSync : onPauseSync,
39189
+ startIcon: isPaused ? /* @__PURE__ */ jsxRuntimeExports.jsx(PlayArrowIcon, {}) : /* @__PURE__ */ jsxRuntimeExports.jsx(PauseIcon, {}),
39190
+ size: "small",
39191
+ children: isPaused ? "Resume" : "Pause"
39192
+ }
39193
+ )
39194
+ ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx(
39195
+ Button,
39196
+ {
39197
+ variant: "contained",
39198
+ color: "primary",
39199
+ onClick: onStartSync,
39200
+ startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(PlayCircleOutlineIcon, {}),
39201
+ size: "small",
39202
+ children: "Start Sync"
39203
+ }
39204
+ )
39205
+ ] }),
39206
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", flexWrap: "wrap", children: [
39207
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
39208
+ Button,
39209
+ {
39210
+ variant: "outlined",
39211
+ color: "warning",
39212
+ onClick: onClearSync,
39213
+ startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(ClearAllIcon, {}),
39214
+ size: "small",
39215
+ disabled: isManualSyncing && !isPaused,
39216
+ children: "Clear Sync"
39217
+ }
39218
+ ),
39219
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
39220
+ Button,
39221
+ {
39222
+ variant: "outlined",
39223
+ onClick: onEditLyrics,
39224
+ startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(EditNoteIcon, {}),
39225
+ size: "small",
39226
+ disabled: isManualSyncing && !isPaused,
39227
+ children: "Edit Lyrics"
39228
+ }
39229
+ ),
39230
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Divider, { orientation: "vertical", flexItem: true, sx: { mx: 0.5 } }),
39231
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
39232
+ Button,
39233
+ {
39234
+ variant: "outlined",
39235
+ onClick: onUnsyncFromCursor,
39236
+ startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(BlockIcon, {}),
39237
+ size: "small",
39238
+ disabled: !canUnsyncFromCursor || isManualSyncing && !isPaused,
39239
+ children: "Unsync from Cursor"
39240
+ }
39241
+ ),
39242
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
39243
+ Button,
39244
+ {
39245
+ variant: "outlined",
39246
+ onClick: onEditSelectedWord,
39247
+ startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(EditIcon, {}),
39248
+ size: "small",
39249
+ disabled: !hasSelectedWords || selectedWordCount !== 1,
39250
+ children: "Edit Word"
39251
+ }
39252
+ ),
39253
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
39254
+ Button,
39255
+ {
39256
+ variant: "outlined",
39257
+ color: "error",
39258
+ onClick: onDeleteSelected,
39259
+ startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(DeleteIcon, {}),
39260
+ size: "small",
39261
+ disabled: !hasSelectedWords || isManualSyncing && !isPaused,
39262
+ children: [
39263
+ "Delete",
39264
+ hasSelectedWords && selectedWordCount > 0 ? ` (${selectedWordCount})` : ""
39265
+ ]
39266
+ }
39267
+ )
39268
+ ] })
39269
+ ] });
39270
+ });
39271
+ const MIN_ZOOM_SECONDS = 4.5;
39272
+ const MAX_ZOOM_SECONDS = 24;
39273
+ const ZOOM_STEPS = 50;
39274
+ function getAllWords(segments) {
39275
+ return segments.flatMap((s) => s.words);
39276
+ }
39277
+ function cloneSegments(segments) {
39278
+ return JSON.parse(JSON.stringify(segments));
39279
+ }
39280
+ const LyricsSynchronizer = reactExports.memo(function LyricsSynchronizer2({
39281
+ segments: initialSegments,
39282
+ currentTime,
39283
+ onPlaySegment,
39284
+ onSave,
39285
+ onCancel,
39286
+ setModalSpacebarHandler
39287
+ }) {
39288
+ const [workingSegments, setWorkingSegments] = reactExports.useState(
39289
+ () => cloneSegments(initialSegments)
39290
+ );
39291
+ const allWords = reactExports.useMemo(() => getAllWords(workingSegments), [workingSegments]);
39292
+ const audioDuration = reactExports.useMemo(() => {
39293
+ if (typeof window.getAudioDuration === "function") {
39294
+ const duration2 = window.getAudioDuration();
39295
+ return duration2 > 0 ? duration2 : 300;
39296
+ }
39297
+ return 300;
39298
+ }, []);
39299
+ const [zoomSeconds, setZoomSeconds] = reactExports.useState(12);
39300
+ const [visibleStartTime, setVisibleStartTime] = reactExports.useState(0);
39301
+ const visibleEndTime = reactExports.useMemo(
39302
+ () => Math.min(visibleStartTime + zoomSeconds, audioDuration),
39303
+ [visibleStartTime, zoomSeconds, audioDuration]
39304
+ );
39305
+ const [isManualSyncing, setIsManualSyncing] = reactExports.useState(false);
39306
+ const [isPaused, setIsPaused] = reactExports.useState(false);
39307
+ const [syncWordIndex, setSyncWordIndex] = reactExports.useState(-1);
39308
+ const [isSpacebarPressed, setIsSpacebarPressed] = reactExports.useState(false);
39309
+ const wordStartTimeRef = reactExports.useRef(null);
39310
+ const spacebarPressTimeRef = reactExports.useRef(null);
39311
+ const currentTimeRef = reactExports.useRef(currentTime);
39312
+ const [selectedWordIds, setSelectedWordIds] = reactExports.useState(/* @__PURE__ */ new Set());
39313
+ const [showEditLyricsModal, setShowEditLyricsModal] = reactExports.useState(false);
39314
+ const [editLyricsText, setEditLyricsText] = reactExports.useState("");
39315
+ const [showEditWordModal, setShowEditWordModal] = reactExports.useState(false);
39316
+ const [editWordText, setEditWordText] = reactExports.useState("");
39317
+ const [editWordId, setEditWordId] = reactExports.useState(null);
39318
+ reactExports.useEffect(() => {
39319
+ currentTimeRef.current = currentTime;
39320
+ }, [currentTime]);
39321
+ reactExports.useEffect(() => {
39322
+ if (isManualSyncing && !isPaused && currentTime > 0) {
39323
+ if (currentTime > visibleEndTime - zoomSeconds * 0.1) {
39324
+ const newStart = Math.max(0, currentTime - zoomSeconds * 0.1);
39325
+ setVisibleStartTime(newStart);
39326
+ } else if (currentTime < visibleStartTime) {
39327
+ setVisibleStartTime(Math.max(0, currentTime - 1));
39328
+ }
39329
+ }
39330
+ }, [currentTime, isManualSyncing, isPaused, visibleStartTime, visibleEndTime, zoomSeconds]);
39331
+ const handleZoomChange = reactExports.useCallback((_, value) => {
39332
+ const zoomValue = value;
39333
+ const newZoomSeconds = MIN_ZOOM_SECONDS + zoomValue / ZOOM_STEPS * (MAX_ZOOM_SECONDS - MIN_ZOOM_SECONDS);
39334
+ setZoomSeconds(newZoomSeconds);
39335
+ }, []);
39336
+ const sliderValue = reactExports.useMemo(() => {
39337
+ return (zoomSeconds - MIN_ZOOM_SECONDS) / (MAX_ZOOM_SECONDS - MIN_ZOOM_SECONDS) * ZOOM_STEPS;
39338
+ }, [zoomSeconds]);
39339
+ const handleScrollChange = reactExports.useCallback((newStartTime) => {
39340
+ setVisibleStartTime(newStartTime);
39341
+ }, []);
39342
+ const updateWords = reactExports.useCallback((newWords) => {
39343
+ setWorkingSegments((prevSegments) => {
39344
+ const newSegments = cloneSegments(prevSegments);
39345
+ const wordMap = new Map(newWords.map((w) => [w.id, w]));
39346
+ for (const segment of newSegments) {
39347
+ segment.words = segment.words.map((w) => wordMap.get(w.id) || w);
39348
+ const timedWords = segment.words.filter(
39349
+ (w) => w.start_time !== null && w.end_time !== null
39350
+ );
39351
+ if (timedWords.length > 0) {
39352
+ segment.start_time = Math.min(...timedWords.map((w) => w.start_time));
39353
+ segment.end_time = Math.max(...timedWords.map((w) => w.end_time));
39354
+ } else {
39355
+ segment.start_time = null;
39356
+ segment.end_time = null;
39357
+ }
39358
+ }
39359
+ return newSegments;
39360
+ });
39361
+ }, []);
39362
+ const [isPlaying, setIsPlaying] = reactExports.useState(false);
39363
+ reactExports.useEffect(() => {
39364
+ const checkPlaying = () => {
39365
+ setIsPlaying(typeof window.isAudioPlaying === "boolean" ? window.isAudioPlaying : false);
39366
+ };
39367
+ checkPlaying();
39368
+ const interval = setInterval(checkPlaying, 100);
39369
+ return () => clearInterval(interval);
39370
+ }, []);
39371
+ const handlePlayAudio = reactExports.useCallback(() => {
39372
+ if (onPlaySegment) {
39373
+ onPlaySegment(currentTimeRef.current);
39374
+ }
39375
+ }, [onPlaySegment]);
39376
+ const handleStopAudio = reactExports.useCallback(() => {
39377
+ if (typeof window.toggleAudioPlayback === "function" && window.isAudioPlaying) {
39378
+ window.toggleAudioPlayback();
39379
+ }
39380
+ if (isManualSyncing) {
39381
+ setIsManualSyncing(false);
39382
+ setIsPaused(false);
39383
+ setIsSpacebarPressed(false);
39384
+ }
39385
+ }, [isManualSyncing]);
39386
+ const handleStartSync = reactExports.useCallback(() => {
39387
+ if (isManualSyncing) {
39388
+ setIsManualSyncing(false);
39389
+ setIsPaused(false);
39390
+ setSyncWordIndex(-1);
39391
+ setIsSpacebarPressed(false);
39392
+ handleStopAudio();
39393
+ return;
39394
+ }
39395
+ const firstUnsyncedIndex = allWords.findIndex(
39396
+ (w) => w.start_time === null || w.end_time === null
39397
+ );
39398
+ const startIndex = firstUnsyncedIndex !== -1 ? firstUnsyncedIndex : 0;
39399
+ setIsManualSyncing(true);
39400
+ setIsPaused(false);
39401
+ setSyncWordIndex(startIndex);
39402
+ setIsSpacebarPressed(false);
39403
+ if (onPlaySegment) {
39404
+ onPlaySegment(Math.max(0, currentTimeRef.current - 1));
39405
+ }
39406
+ }, [isManualSyncing, allWords, onPlaySegment, handleStopAudio]);
39407
+ const handlePauseSync = reactExports.useCallback(() => {
39408
+ setIsPaused(true);
39409
+ handleStopAudio();
39410
+ }, [handleStopAudio]);
39411
+ const handleResumeSync = reactExports.useCallback(() => {
39412
+ setIsPaused(false);
39413
+ const firstUnsyncedIndex = allWords.findIndex(
39414
+ (w) => w.start_time === null || w.end_time === null
39415
+ );
39416
+ if (firstUnsyncedIndex !== -1 && firstUnsyncedIndex !== syncWordIndex) {
39417
+ setSyncWordIndex(firstUnsyncedIndex);
39418
+ }
39419
+ if (onPlaySegment) {
39420
+ onPlaySegment(currentTimeRef.current);
39421
+ }
39422
+ }, [allWords, syncWordIndex, onPlaySegment]);
39423
+ const handleClearSync = reactExports.useCallback(() => {
39424
+ setWorkingSegments((prevSegments) => {
39425
+ const newSegments = cloneSegments(prevSegments);
39426
+ for (const segment of newSegments) {
39427
+ for (const word of segment.words) {
39428
+ word.start_time = null;
39429
+ word.end_time = null;
39430
+ }
39431
+ segment.start_time = null;
39432
+ segment.end_time = null;
39433
+ }
39434
+ return newSegments;
39435
+ });
39436
+ setSyncWordIndex(-1);
39437
+ }, []);
39438
+ const handleUnsyncFromCursor = reactExports.useCallback(() => {
39439
+ const cursorTime = currentTimeRef.current;
39440
+ setWorkingSegments((prevSegments) => {
39441
+ const newSegments = cloneSegments(prevSegments);
39442
+ for (const segment of newSegments) {
39443
+ for (const word of segment.words) {
39444
+ if (word.start_time !== null && word.start_time > cursorTime) {
39445
+ word.start_time = null;
39446
+ word.end_time = null;
39447
+ }
39448
+ }
39449
+ const timedWords = segment.words.filter(
39450
+ (w) => w.start_time !== null && w.end_time !== null
39451
+ );
39452
+ if (timedWords.length > 0) {
39453
+ segment.start_time = Math.min(...timedWords.map((w) => w.start_time));
39454
+ segment.end_time = Math.max(...timedWords.map((w) => w.end_time));
39455
+ } else {
39456
+ segment.start_time = null;
39457
+ segment.end_time = null;
39458
+ }
39459
+ }
39460
+ return newSegments;
39461
+ });
39462
+ }, []);
39463
+ const canUnsyncFromCursor = reactExports.useMemo(() => {
39464
+ const cursorTime = currentTimeRef.current;
39465
+ return allWords.some(
39466
+ (w) => w.start_time !== null && w.start_time > cursorTime
39467
+ );
39468
+ }, [allWords, currentTime]);
39469
+ const handleEditLyrics = reactExports.useCallback(() => {
39470
+ const text = workingSegments.map((s) => s.text).join("\n");
39471
+ setEditLyricsText(text);
39472
+ setShowEditLyricsModal(true);
39473
+ }, [workingSegments]);
39474
+ const handleSaveEditedLyrics = reactExports.useCallback(() => {
39475
+ const lines = editLyricsText.split("\n").filter((l) => l.trim());
39476
+ const newSegments = lines.map((line2, idx) => {
39477
+ const words = line2.trim().split(/\s+/).map((text, wIdx) => ({
39478
+ id: `word-${idx}-${wIdx}-${Date.now()}`,
39479
+ text,
39480
+ start_time: null,
39481
+ end_time: null,
39482
+ confidence: 1
39483
+ }));
39484
+ return {
39485
+ id: `segment-${idx}-${Date.now()}`,
39486
+ text: line2.trim(),
39487
+ words,
39488
+ start_time: null,
39489
+ end_time: null
39490
+ };
39491
+ });
39492
+ setWorkingSegments(newSegments);
39493
+ setShowEditLyricsModal(false);
39494
+ setSyncWordIndex(-1);
39495
+ }, [editLyricsText]);
39496
+ const handleEditSelectedWord = reactExports.useCallback(() => {
39497
+ if (selectedWordIds.size !== 1) return;
39498
+ const wordId = Array.from(selectedWordIds)[0];
39499
+ const word = allWords.find((w) => w.id === wordId);
39500
+ if (word) {
39501
+ setEditWordId(wordId);
39502
+ setEditWordText(word.text);
39503
+ setShowEditWordModal(true);
39504
+ }
39505
+ }, [selectedWordIds, allWords]);
39506
+ const handleSaveEditedWord = reactExports.useCallback(() => {
39507
+ if (!editWordId) return;
39508
+ const newText = editWordText.trim();
39509
+ if (!newText) return;
39510
+ const newWords = newText.split(/\s+/);
39511
+ if (newWords.length === 1) {
39512
+ const updatedWords = allWords.map(
39513
+ (w) => w.id === editWordId ? { ...w, text: newWords[0] } : w
39514
+ );
39515
+ updateWords(updatedWords);
39516
+ } else {
39517
+ const originalWord = allWords.find((w) => w.id === editWordId);
39518
+ if (!originalWord) return;
39519
+ setWorkingSegments((prevSegments) => {
39520
+ const newSegments = cloneSegments(prevSegments);
39521
+ for (const segment of newSegments) {
39522
+ const wordIndex = segment.words.findIndex((w) => w.id === editWordId);
39523
+ if (wordIndex !== -1) {
39524
+ const newWordObjects = newWords.map((text, idx) => ({
39525
+ id: idx === 0 ? editWordId : `${editWordId}-split-${idx}`,
39526
+ text,
39527
+ start_time: idx === 0 ? originalWord.start_time : null,
39528
+ end_time: idx === 0 ? originalWord.end_time : null,
39529
+ confidence: 1
39530
+ }));
39531
+ segment.words.splice(wordIndex, 1, ...newWordObjects);
39532
+ segment.text = segment.words.map((w) => w.text).join(" ");
39533
+ break;
39534
+ }
39535
+ }
39536
+ return newSegments;
39537
+ });
39538
+ }
39539
+ setShowEditWordModal(false);
39540
+ setEditWordId(null);
39541
+ setEditWordText("");
39542
+ setSelectedWordIds(/* @__PURE__ */ new Set());
39543
+ }, [editWordId, editWordText, allWords, updateWords]);
39544
+ const handleDeleteSelected = reactExports.useCallback(() => {
39545
+ if (selectedWordIds.size === 0) return;
39546
+ setWorkingSegments((prevSegments) => {
39547
+ const newSegments = cloneSegments(prevSegments);
39548
+ for (const segment of newSegments) {
39549
+ segment.words = segment.words.filter((w) => !selectedWordIds.has(w.id));
39550
+ segment.text = segment.words.map((w) => w.text).join(" ");
39551
+ const timedWords = segment.words.filter(
39552
+ (w) => w.start_time !== null && w.end_time !== null
39553
+ );
39554
+ if (timedWords.length > 0) {
39555
+ segment.start_time = Math.min(...timedWords.map((w) => w.start_time));
39556
+ segment.end_time = Math.max(...timedWords.map((w) => w.end_time));
39557
+ } else {
39558
+ segment.start_time = null;
39559
+ segment.end_time = null;
39560
+ }
39561
+ }
39562
+ return newSegments.filter((s) => s.words.length > 0);
39563
+ });
39564
+ setSelectedWordIds(/* @__PURE__ */ new Set());
39565
+ }, [selectedWordIds]);
39566
+ const handleWordClick = reactExports.useCallback((wordId, event) => {
39567
+ if (event.shiftKey || event.ctrlKey || event.metaKey) {
39568
+ setSelectedWordIds((prev2) => {
39569
+ const newSet = new Set(prev2);
39570
+ if (newSet.has(wordId)) {
39571
+ newSet.delete(wordId);
39572
+ } else {
39573
+ newSet.add(wordId);
39574
+ }
39575
+ return newSet;
39576
+ });
39577
+ } else {
39578
+ setSelectedWordIds(/* @__PURE__ */ new Set([wordId]));
39579
+ }
39580
+ }, []);
39581
+ const handleBackgroundClick = reactExports.useCallback(() => {
39582
+ setSelectedWordIds(/* @__PURE__ */ new Set());
39583
+ }, []);
39584
+ const handleWordTimingChange = reactExports.useCallback((wordId, newStartTime, newEndTime) => {
39585
+ setWorkingSegments((prevSegments) => {
39586
+ const newSegments = cloneSegments(prevSegments);
39587
+ for (const segment of newSegments) {
39588
+ const word = segment.words.find((w) => w.id === wordId);
39589
+ if (word) {
39590
+ word.start_time = Math.max(0, newStartTime);
39591
+ word.end_time = Math.max(word.start_time + 0.05, newEndTime);
39592
+ const timedWords = segment.words.filter(
39593
+ (w) => w.start_time !== null && w.end_time !== null
39594
+ );
39595
+ if (timedWords.length > 0) {
39596
+ segment.start_time = Math.min(...timedWords.map((w) => w.start_time));
39597
+ segment.end_time = Math.max(...timedWords.map((w) => w.end_time));
39598
+ }
39599
+ break;
39600
+ }
39601
+ }
39602
+ return newSegments;
39603
+ });
39604
+ }, []);
39605
+ const handleWordsMove = reactExports.useCallback((updates) => {
39606
+ setWorkingSegments((prevSegments) => {
39607
+ const newSegments = cloneSegments(prevSegments);
39608
+ const updateMap = new Map(updates.map((u) => [u.wordId, u]));
39609
+ for (const segment of newSegments) {
39610
+ for (const word of segment.words) {
39611
+ const update = updateMap.get(word.id);
39612
+ if (update) {
39613
+ word.start_time = update.newStartTime;
39614
+ word.end_time = update.newEndTime;
39615
+ }
39616
+ }
39617
+ const timedWords = segment.words.filter(
39618
+ (w) => w.start_time !== null && w.end_time !== null
39619
+ );
39620
+ if (timedWords.length > 0) {
39621
+ segment.start_time = Math.min(...timedWords.map((w) => w.start_time));
39622
+ segment.end_time = Math.max(...timedWords.map((w) => w.end_time));
39623
+ }
39624
+ }
39625
+ return newSegments;
39626
+ });
39627
+ }, []);
39628
+ const handleTimeBarClick = reactExports.useCallback((time) => {
39629
+ const newStart = Math.max(0, time - zoomSeconds / 2);
39630
+ setVisibleStartTime(Math.min(newStart, Math.max(0, audioDuration - zoomSeconds)));
39631
+ if (onPlaySegment) {
39632
+ onPlaySegment(time);
39633
+ setTimeout(() => {
39634
+ if (typeof window.toggleAudioPlayback === "function" && window.isAudioPlaying) {
39635
+ window.toggleAudioPlayback();
39636
+ }
39637
+ }, 50);
39638
+ }
39639
+ }, [zoomSeconds, audioDuration, onPlaySegment]);
39640
+ const handleSelectionComplete = reactExports.useCallback((wordIds) => {
39641
+ setSelectedWordIds(new Set(wordIds));
39642
+ }, []);
39643
+ const handleKeyDown = reactExports.useCallback((e) => {
39644
+ if (e.code !== "Space") return;
39645
+ if (!isManualSyncing || isPaused) return;
39646
+ if (syncWordIndex < 0 || syncWordIndex >= allWords.length) return;
39647
+ e.preventDefault();
39648
+ e.stopPropagation();
39649
+ if (isSpacebarPressed) return;
39650
+ setIsSpacebarPressed(true);
39651
+ wordStartTimeRef.current = currentTimeRef.current;
39652
+ spacebarPressTimeRef.current = Date.now();
39653
+ const newWords = [...allWords];
39654
+ const currentWord = newWords[syncWordIndex];
39655
+ currentWord.start_time = currentTimeRef.current;
39656
+ if (syncWordIndex > 0) {
39657
+ const prevWord = newWords[syncWordIndex - 1];
39658
+ if (prevWord.start_time !== null && prevWord.end_time === null) {
39659
+ const gap2 = currentTimeRef.current - prevWord.start_time;
39660
+ if (gap2 > 1) {
39661
+ prevWord.end_time = prevWord.start_time + 0.5;
39662
+ } else {
39663
+ prevWord.end_time = currentTimeRef.current - 5e-3;
39664
+ }
39665
+ }
39666
+ }
39667
+ updateWords(newWords);
39668
+ }, [isManualSyncing, isPaused, syncWordIndex, allWords, isSpacebarPressed, updateWords]);
39669
+ const handleKeyUp = reactExports.useCallback((e) => {
39670
+ if (e.code !== "Space") return;
39671
+ if (!isManualSyncing || isPaused) return;
39672
+ if (!isSpacebarPressed) return;
39673
+ e.preventDefault();
39674
+ e.stopPropagation();
39675
+ setIsSpacebarPressed(false);
39676
+ const pressDuration = spacebarPressTimeRef.current ? Date.now() - spacebarPressTimeRef.current : 0;
39677
+ const isTap = pressDuration < 200;
39678
+ const newWords = [...allWords];
39679
+ const currentWord = newWords[syncWordIndex];
39680
+ if (isTap) {
39681
+ currentWord.end_time = (wordStartTimeRef.current || currentTimeRef.current) + 0.5;
39682
+ } else {
39683
+ currentWord.end_time = currentTimeRef.current;
39684
+ }
39685
+ updateWords(newWords);
39686
+ if (syncWordIndex < allWords.length - 1) {
39687
+ setSyncWordIndex(syncWordIndex + 1);
39688
+ } else {
39689
+ setIsManualSyncing(false);
39690
+ setSyncWordIndex(-1);
39691
+ handleStopAudio();
39692
+ }
39693
+ wordStartTimeRef.current = null;
39694
+ spacebarPressTimeRef.current = null;
39695
+ }, [isManualSyncing, isPaused, isSpacebarPressed, syncWordIndex, allWords, updateWords, handleStopAudio]);
39696
+ const handleSpacebar = reactExports.useCallback((e) => {
39697
+ if (e.type === "keydown") {
39698
+ handleKeyDown(e);
39699
+ } else if (e.type === "keyup") {
39700
+ handleKeyUp(e);
39701
+ }
39702
+ }, [handleKeyDown, handleKeyUp]);
39703
+ const spacebarHandlerRef = reactExports.useRef(handleSpacebar);
39704
+ spacebarHandlerRef.current = handleSpacebar;
39705
+ reactExports.useEffect(() => {
39706
+ const handler = (e) => {
39707
+ if (e.code === "Space") {
39708
+ e.preventDefault();
39709
+ e.stopPropagation();
39710
+ spacebarHandlerRef.current(e);
39711
+ }
39712
+ };
39713
+ setModalSpacebarHandler(() => handler);
39714
+ return () => {
39715
+ setModalSpacebarHandler(void 0);
39716
+ };
39717
+ }, [setModalSpacebarHandler]);
39718
+ const handleSave = reactExports.useCallback(() => {
39719
+ onSave(workingSegments);
39720
+ }, [workingSegments, onSave]);
39721
+ const stats = reactExports.useMemo(() => {
39722
+ const total = allWords.length;
39723
+ const synced = allWords.filter(
39724
+ (w) => w.start_time !== null && w.end_time !== null
39725
+ ).length;
39726
+ return { total, synced, remaining: total - synced };
39727
+ }, [allWords]);
39728
+ const getInstructionText = reactExports.useCallback(() => {
39729
+ if (isManualSyncing) {
39730
+ if (isSpacebarPressed) {
39731
+ return { primary: "⏱️ Holding... release when word ends", secondary: "Release spacebar when the word finishes" };
39732
+ }
39733
+ if (stats.remaining === 0) {
39734
+ return { primary: "✅ All words synced!", secondary: 'Click "Stop Sync" then "Apply" to save' };
39735
+ }
39736
+ return { primary: "👆 Press SPACEBAR when you hear each word", secondary: "Tap for short words, hold for longer words" };
39737
+ }
39738
+ if (stats.synced === 0) {
39739
+ return { primary: 'Click "Start Sync" to begin timing words', secondary: "Audio will play and you'll tap spacebar for each word" };
39740
+ }
39741
+ if (stats.remaining > 0) {
39742
+ return { primary: `${stats.remaining} words remaining to sync`, secondary: 'Click "Start Sync" to continue, or "Unsync from Cursor" to re-sync from a point' };
39743
+ }
39744
+ return { primary: "✅ All words synced!", secondary: 'Click "Apply" to save changes, or make adjustments first' };
39745
+ }, [isManualSyncing, isSpacebarPressed, stats.synced, stats.remaining]);
39746
+ const instruction = getInstructionText();
39747
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { display: "flex", flexDirection: "column", height: "100%", gap: 1 }, children: [
39748
+ /* @__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: [
39749
+ stats.synced,
39750
+ " / ",
39751
+ stats.total,
39752
+ " words synced",
39753
+ stats.remaining > 0 && ` (${stats.remaining} remaining)`
39754
+ ] }) }),
39755
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
39756
+ Box,
39757
+ {
39758
+ sx: {
39759
+ height: 56,
39760
+ flexShrink: 0
38964
39761
  },
38965
- segment.id
38966
- );
38967
- }) })
39762
+ children: /* @__PURE__ */ jsxRuntimeExports.jsxs(
39763
+ Paper,
39764
+ {
39765
+ sx: {
39766
+ p: 1.5,
39767
+ height: "100%",
39768
+ bgcolor: isManualSyncing ? "info.main" : "grey.100",
39769
+ color: isManualSyncing ? "info.contrastText" : "text.primary",
39770
+ display: "flex",
39771
+ flexDirection: "column",
39772
+ justifyContent: "center",
39773
+ overflow: "hidden",
39774
+ boxSizing: "border-box"
39775
+ },
39776
+ children: [
39777
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "body2", sx: { fontWeight: 500, lineHeight: 1.3 }, children: instruction.primary }),
39778
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "caption", sx: { opacity: 0.85, display: "block", lineHeight: 1.3 }, children: instruction.secondary })
39779
+ ]
39780
+ }
39781
+ )
39782
+ }
39783
+ ),
39784
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { sx: { height: 88, flexShrink: 0 }, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
39785
+ SyncControls,
39786
+ {
39787
+ isManualSyncing,
39788
+ isPaused,
39789
+ onStartSync: handleStartSync,
39790
+ onPauseSync: handlePauseSync,
39791
+ onResumeSync: handleResumeSync,
39792
+ onClearSync: handleClearSync,
39793
+ onEditLyrics: handleEditLyrics,
39794
+ onPlay: handlePlayAudio,
39795
+ onStop: handleStopAudio,
39796
+ isPlaying,
39797
+ hasSelectedWords: selectedWordIds.size > 0,
39798
+ selectedWordCount: selectedWordIds.size,
39799
+ onUnsyncFromCursor: handleUnsyncFromCursor,
39800
+ onEditSelectedWord: handleEditSelectedWord,
39801
+ onDeleteSelected: handleDeleteSelected,
39802
+ canUnsyncFromCursor
39803
+ }
39804
+ ) }),
39805
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { sx: { height: 44, flexShrink: 0 }, children: isManualSyncing && /* @__PURE__ */ jsxRuntimeExports.jsx(
39806
+ UpcomingWordsBar,
39807
+ {
39808
+ words: allWords,
39809
+ syncWordIndex,
39810
+ isManualSyncing
39811
+ }
39812
+ ) }),
39813
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { sx: { flexGrow: 1, minHeight: 200 }, children: /* @__PURE__ */ jsxRuntimeExports.jsx(
39814
+ TimelineCanvas,
39815
+ {
39816
+ words: allWords,
39817
+ segments: workingSegments,
39818
+ visibleStartTime,
39819
+ visibleEndTime,
39820
+ currentTime,
39821
+ selectedWordIds,
39822
+ onWordClick: handleWordClick,
39823
+ onBackgroundClick: handleBackgroundClick,
39824
+ onTimeBarClick: handleTimeBarClick,
39825
+ onSelectionComplete: handleSelectionComplete,
39826
+ onWordTimingChange: handleWordTimingChange,
39827
+ onWordsMove: handleWordsMove,
39828
+ syncWordIndex,
39829
+ isManualSyncing,
39830
+ onScrollChange: handleScrollChange,
39831
+ audioDuration,
39832
+ zoomSeconds,
39833
+ height: 200
39834
+ }
39835
+ ) }),
39836
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: 2, px: 2 }, children: [
39837
+ /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomInIcon, { color: "action", fontSize: "small" }),
39838
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
39839
+ Slider,
39840
+ {
39841
+ value: sliderValue,
39842
+ onChange: handleZoomChange,
39843
+ min: 0,
39844
+ max: ZOOM_STEPS,
39845
+ step: 1,
39846
+ sx: { flexGrow: 1 },
39847
+ disabled: isManualSyncing && !isPaused
39848
+ }
39849
+ ),
39850
+ /* @__PURE__ */ jsxRuntimeExports.jsx(ZoomOutIcon, { color: "action", fontSize: "small" }),
39851
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(Typography, { variant: "caption", color: "text.secondary", sx: { minWidth: 60 }, children: [
39852
+ zoomSeconds.toFixed(1),
39853
+ "s view"
39854
+ ] })
39855
+ ] }),
39856
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { display: "flex", justifyContent: "flex-end", gap: 2, pt: 2, borderTop: 1, borderColor: "divider" }, children: [
39857
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { onClick: onCancel, color: "inherit", children: "Cancel" }),
39858
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
39859
+ Button,
39860
+ {
39861
+ onClick: handleSave,
39862
+ variant: "contained",
39863
+ color: "primary",
39864
+ disabled: isManualSyncing && !isPaused,
39865
+ children: "Apply"
39866
+ }
39867
+ )
39868
+ ] }),
39869
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
39870
+ Dialog,
39871
+ {
39872
+ open: showEditLyricsModal,
39873
+ onClose: () => setShowEditLyricsModal(false),
39874
+ maxWidth: "md",
39875
+ fullWidth: true,
39876
+ children: [
39877
+ /* @__PURE__ */ jsxRuntimeExports.jsx(DialogTitle, { children: "Edit Lyrics" }),
39878
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(DialogContent, { children: [
39879
+ /* @__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." }),
39880
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
39881
+ TextField,
39882
+ {
39883
+ multiline: true,
39884
+ rows: 15,
39885
+ fullWidth: true,
39886
+ value: editLyricsText,
39887
+ onChange: (e) => setEditLyricsText(e.target.value),
39888
+ placeholder: "Enter lyrics, one line per segment..."
39889
+ }
39890
+ )
39891
+ ] }),
39892
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(DialogActions, { children: [
39893
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { onClick: () => setShowEditLyricsModal(false), children: "Cancel" }),
39894
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { onClick: handleSaveEditedLyrics, variant: "contained", color: "warning", children: "Save & Reset Timing" })
39895
+ ] })
39896
+ ]
39897
+ }
39898
+ ),
39899
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
39900
+ Dialog,
39901
+ {
39902
+ open: showEditWordModal,
39903
+ onClose: () => setShowEditWordModal(false),
39904
+ maxWidth: "xs",
39905
+ fullWidth: true,
39906
+ children: [
39907
+ /* @__PURE__ */ jsxRuntimeExports.jsx(DialogTitle, { children: "Edit Word" }),
39908
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(DialogContent, { children: [
39909
+ /* @__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." }),
39910
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
39911
+ TextField,
39912
+ {
39913
+ fullWidth: true,
39914
+ value: editWordText,
39915
+ onChange: (e) => setEditWordText(e.target.value),
39916
+ autoFocus: true,
39917
+ onKeyDown: (e) => {
39918
+ if (e.key === "Enter") {
39919
+ handleSaveEditedWord();
39920
+ }
39921
+ }
39922
+ }
39923
+ )
39924
+ ] }),
39925
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(DialogActions, { children: [
39926
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { onClick: () => setShowEditWordModal(false), children: "Cancel" }),
39927
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { onClick: handleSaveEditedWord, variant: "contained", children: "Save" })
39928
+ ] })
39929
+ ]
39930
+ }
39931
+ )
38968
39932
  ] });
38969
39933
  });
39934
+ function ReplaceAllLyricsModal({
39935
+ open,
39936
+ onClose,
39937
+ onSave,
39938
+ onPlaySegment,
39939
+ currentTime = 0,
39940
+ setModalSpacebarHandler,
39941
+ existingSegments = []
39942
+ }) {
39943
+ const [mode, setMode] = reactExports.useState("selection");
39944
+ const [inputText, setInputText] = reactExports.useState("");
39945
+ const [newSegments, setNewSegments] = reactExports.useState([]);
39946
+ reactExports.useEffect(() => {
39947
+ if (open) {
39948
+ setMode("selection");
39949
+ setInputText("");
39950
+ setNewSegments([]);
39951
+ }
39952
+ }, [open]);
39953
+ const parseInfo = reactExports.useMemo(() => {
39954
+ if (!inputText.trim()) return { lines: 0, words: 0 };
39955
+ const lines = inputText.trim().split("\n").filter((line2) => line2.trim().length > 0);
39956
+ const totalWords = lines.reduce((count, line2) => {
39957
+ return count + line2.trim().split(/\s+/).length;
39958
+ }, 0);
39959
+ return { lines: lines.length, words: totalWords };
39960
+ }, [inputText]);
39961
+ const processLyrics = reactExports.useCallback(() => {
39962
+ if (!inputText.trim()) return;
39963
+ const lines = inputText.trim().split("\n").filter((line2) => line2.trim().length > 0);
39964
+ const segments = [];
39965
+ lines.forEach((line2) => {
39966
+ const words = line2.trim().split(/\s+/).filter((word) => word.length > 0);
39967
+ const segmentWords = words.map((wordText) => ({
39968
+ id: nanoid(),
39969
+ text: wordText,
39970
+ start_time: null,
39971
+ end_time: null,
39972
+ confidence: 1,
39973
+ created_during_correction: true
39974
+ }));
39975
+ segments.push({
39976
+ id: nanoid(),
39977
+ text: line2.trim(),
39978
+ words: segmentWords,
39979
+ start_time: null,
39980
+ end_time: null
39981
+ });
39982
+ });
39983
+ setNewSegments(segments);
39984
+ setMode("resync");
39985
+ }, [inputText]);
39986
+ const handlePasteFromClipboard = reactExports.useCallback(async () => {
39987
+ try {
39988
+ const text = await navigator.clipboard.readText();
39989
+ setInputText(text);
39990
+ } catch (error) {
39991
+ console.error("Failed to read from clipboard:", error);
39992
+ alert("Failed to read from clipboard. Please paste manually.");
39993
+ }
39994
+ }, []);
39995
+ const handleClose = reactExports.useCallback(() => {
39996
+ setMode("selection");
39997
+ setInputText("");
39998
+ setNewSegments([]);
39999
+ onClose();
40000
+ }, [onClose]);
40001
+ const handleSave = reactExports.useCallback((segments) => {
40002
+ onSave(segments);
40003
+ handleClose();
40004
+ }, [onSave, handleClose]);
40005
+ const handleSelectReplace = reactExports.useCallback(() => {
40006
+ setMode("replace");
40007
+ }, []);
40008
+ const handleSelectResync = reactExports.useCallback(() => {
40009
+ setMode("resync");
40010
+ }, []);
40011
+ const handleBackToSelection = reactExports.useCallback(() => {
40012
+ setMode("selection");
40013
+ setInputText("");
40014
+ setNewSegments([]);
40015
+ }, []);
40016
+ const segmentsForSync = mode === "resync" && newSegments.length > 0 ? newSegments : existingSegments;
40017
+ const hasExistingLyrics = existingSegments.length > 0 && existingSegments.some((s) => s.words.length > 0);
40018
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
40019
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
40020
+ ModeSelectionModal,
40021
+ {
40022
+ open: open && mode === "selection",
40023
+ onClose: handleClose,
40024
+ onSelectReplace: handleSelectReplace,
40025
+ onSelectResync: handleSelectResync,
40026
+ hasExistingLyrics
40027
+ }
40028
+ ),
40029
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
40030
+ Dialog,
40031
+ {
40032
+ open: open && mode === "replace",
40033
+ onClose: handleClose,
40034
+ maxWidth: "md",
40035
+ fullWidth: true,
40036
+ PaperProps: {
40037
+ sx: {
40038
+ height: "80vh",
40039
+ maxHeight: "80vh"
40040
+ }
40041
+ },
40042
+ children: [
40043
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(DialogTitle, { sx: { display: "flex", alignItems: "center", gap: 1 }, children: [
40044
+ /* @__PURE__ */ jsxRuntimeExports.jsx(IconButton, { onClick: handleBackToSelection, size: "small", children: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowBackIcon, {}) }),
40045
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { sx: { flex: 1 }, children: "Replace All Lyrics" }),
40046
+ /* @__PURE__ */ jsxRuntimeExports.jsx(IconButton, { onClick: handleClose, sx: { ml: "auto" }, children: /* @__PURE__ */ jsxRuntimeExports.jsx(CloseIcon, {}) })
40047
+ ] }),
40048
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
40049
+ DialogContent,
40050
+ {
40051
+ dividers: true,
40052
+ sx: {
40053
+ display: "flex",
40054
+ flexDirection: "column",
40055
+ flexGrow: 1,
40056
+ overflow: "hidden"
40057
+ },
40058
+ children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { display: "flex", flexDirection: "column", gap: 2, height: "100%" }, children: [
40059
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "h6", gutterBottom: true, children: "Paste your new lyrics below:" }),
40060
+ /* @__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." }),
40061
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { display: "flex", gap: 2, mb: 2 }, children: [
40062
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
40063
+ Button,
40064
+ {
40065
+ variant: "outlined",
40066
+ onClick: handlePasteFromClipboard,
40067
+ startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(ContentPasteIcon, {}),
40068
+ size: "small",
40069
+ children: "Paste from Clipboard"
40070
+ }
40071
+ ),
40072
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(Typography, { variant: "body2", sx: {
40073
+ alignSelf: "center",
40074
+ color: "text.secondary",
40075
+ fontWeight: "medium"
40076
+ }, children: [
40077
+ parseInfo.lines,
40078
+ " lines, ",
40079
+ parseInfo.words,
40080
+ " words"
40081
+ ] })
40082
+ ] }),
40083
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
40084
+ TextField,
40085
+ {
40086
+ multiline: true,
40087
+ rows: 15,
40088
+ value: inputText,
40089
+ onChange: (e) => setInputText(e.target.value),
40090
+ placeholder: "Paste your lyrics here...\nEach line will become a segment\nWords will be separated by spaces",
40091
+ sx: {
40092
+ flexGrow: 1,
40093
+ "& .MuiInputBase-root": {
40094
+ height: "100%",
40095
+ alignItems: "flex-start"
40096
+ }
40097
+ }
40098
+ }
40099
+ )
40100
+ ] })
40101
+ }
40102
+ ),
40103
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(DialogActions, { children: [
40104
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { onClick: handleClose, color: "inherit", children: "Cancel" }),
40105
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
40106
+ Button,
40107
+ {
40108
+ variant: "contained",
40109
+ onClick: processLyrics,
40110
+ disabled: !inputText.trim(),
40111
+ startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(AutoFixHighIcon, {}),
40112
+ children: "Continue to Sync"
40113
+ }
40114
+ )
40115
+ ] })
40116
+ ]
40117
+ }
40118
+ ),
40119
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
40120
+ Dialog,
40121
+ {
40122
+ open: open && mode === "resync",
40123
+ onClose: handleClose,
40124
+ maxWidth: false,
40125
+ fullWidth: true,
40126
+ PaperProps: {
40127
+ sx: {
40128
+ height: "90vh",
40129
+ margin: "5vh 2vw",
40130
+ maxWidth: "calc(100vw - 4vw)",
40131
+ width: "calc(100vw - 4vw)"
40132
+ }
40133
+ },
40134
+ children: [
40135
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(DialogTitle, { sx: { display: "flex", alignItems: "center", gap: 1 }, children: [
40136
+ /* @__PURE__ */ jsxRuntimeExports.jsx(IconButton, { onClick: handleBackToSelection, size: "small", children: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowBackIcon, {}) }),
40137
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { sx: { flex: 1 }, children: newSegments.length > 0 ? "Sync New Lyrics" : "Re-sync Existing Lyrics" }),
40138
+ /* @__PURE__ */ jsxRuntimeExports.jsx(IconButton, { onClick: handleClose, sx: { ml: "auto" }, children: /* @__PURE__ */ jsxRuntimeExports.jsx(CloseIcon, {}) })
40139
+ ] }),
40140
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
40141
+ DialogContent,
40142
+ {
40143
+ dividers: true,
40144
+ sx: {
40145
+ display: "flex",
40146
+ flexDirection: "column",
40147
+ flexGrow: 1,
40148
+ overflow: "hidden",
40149
+ p: 2
40150
+ },
40151
+ children: segmentsForSync.length > 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx(
40152
+ LyricsSynchronizer,
40153
+ {
40154
+ segments: segmentsForSync,
40155
+ currentTime,
40156
+ onPlaySegment,
40157
+ onSave: handleSave,
40158
+ onCancel: handleClose,
40159
+ setModalSpacebarHandler
40160
+ }
40161
+ ) : /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: {
40162
+ display: "flex",
40163
+ flexDirection: "column",
40164
+ alignItems: "center",
40165
+ justifyContent: "center",
40166
+ height: "100%",
40167
+ gap: 2
40168
+ }, children: [
40169
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "h6", color: "text.secondary", children: "No lyrics to sync" }),
40170
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "body2", color: "text.secondary", children: "Go back and paste new lyrics, or close this modal." }),
40171
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
40172
+ Button,
40173
+ {
40174
+ variant: "outlined",
40175
+ onClick: handleBackToSelection,
40176
+ startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowBackIcon, {}),
40177
+ children: "Back to Selection"
40178
+ }
40179
+ )
40180
+ ] })
40181
+ }
40182
+ )
40183
+ ]
40184
+ }
40185
+ )
40186
+ ] });
40187
+ }
38970
40188
  const ANNOTATION_TYPES = [
38971
40189
  {
38972
40190
  value: "SOUND_ALIKE",
@@ -39866,7 +41084,7 @@ function AudioPlayer({ apiClient, onTimeUpdate, audioHash }) {
39866
41084
  audioRef.current.currentTime = time;
39867
41085
  setCurrentTime(time);
39868
41086
  };
39869
- const formatTime = (seconds) => {
41087
+ const formatTime2 = (seconds) => {
39870
41088
  const mins = Math.floor(seconds / 60);
39871
41089
  const secs = Math.floor(seconds % 60);
39872
41090
  return `${mins}:${secs.toString().padStart(2, "0")}`;
@@ -39918,7 +41136,7 @@ function AudioPlayer({ apiClient, onTimeUpdate, audioHash }) {
39918
41136
  children: isPlaying ? /* @__PURE__ */ jsxRuntimeExports.jsx(PauseIcon, { fontSize: "small" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(PlayArrowIcon, { fontSize: "small" })
39919
41137
  }
39920
41138
  ),
39921
- /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "body2", sx: { minWidth: 32, fontSize: "0.75rem" }, children: formatTime(currentTime) }),
41139
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "body2", sx: { minWidth: 32, fontSize: "0.75rem" }, children: formatTime2(currentTime) }),
39922
41140
  /* @__PURE__ */ jsxRuntimeExports.jsx(
39923
41141
  Slider,
39924
41142
  {
@@ -39940,7 +41158,7 @@ function AudioPlayer({ apiClient, onTimeUpdate, audioHash }) {
39940
41158
  }
39941
41159
  }
39942
41160
  ),
39943
- /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "body2", sx: { minWidth: 32, fontSize: "0.75rem" }, children: formatTime(duration2) })
41161
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "body2", sx: { minWidth: 32, fontSize: "0.75rem" }, children: formatTime2(duration2) })
39944
41162
  ] });
39945
41163
  }
39946
41164
  function Header({
@@ -39964,7 +41182,9 @@ function Header({
39964
41182
  onUndo,
39965
41183
  onRedo,
39966
41184
  canUndo,
39967
- canRedo
41185
+ canRedo,
41186
+ annotationsEnabled = true,
41187
+ onAnnotationsToggle
39968
41188
  }) {
39969
41189
  var _a, _b, _c;
39970
41190
  const theme2 = useTheme();
@@ -40011,17 +41231,34 @@ function Header({
40011
41231
  mb: 1
40012
41232
  }, children: [
40013
41233
  /* @__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
- )
41234
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: 1 }, children: [
41235
+ !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(
41236
+ Chip,
41237
+ {
41238
+ icon: /* @__PURE__ */ jsxRuntimeExports.jsx(RateReviewIcon, {}),
41239
+ label: annotationsEnabled ? "Feedback On" : "Feedback Off",
41240
+ onClick: () => onAnnotationsToggle(!annotationsEnabled),
41241
+ color: annotationsEnabled ? "primary" : "default",
41242
+ variant: annotationsEnabled ? "filled" : "outlined",
41243
+ size: "small",
41244
+ sx: {
41245
+ cursor: "pointer",
41246
+ "& .MuiChip-icon": { fontSize: "1rem" }
41247
+ }
41248
+ }
41249
+ ) }),
41250
+ isReadOnly && /* @__PURE__ */ jsxRuntimeExports.jsx(
41251
+ Button,
41252
+ {
41253
+ variant: "outlined",
41254
+ size: "small",
41255
+ startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(UploadFileIcon, {}),
41256
+ onClick: onFileLoad,
41257
+ fullWidth: isMobile,
41258
+ children: "Load File"
41259
+ }
41260
+ )
41261
+ ] })
40025
41262
  ] }),
40026
41263
  /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: {
40027
41264
  display: "flex",
@@ -40859,7 +42096,9 @@ const MemoizedHeader = reactExports.memo(function MemoizedHeader2({
40859
42096
  onRedo,
40860
42097
  canUndo,
40861
42098
  canRedo,
40862
- onUnCorrectAll
42099
+ onUnCorrectAll,
42100
+ annotationsEnabled,
42101
+ onAnnotationsToggle
40863
42102
  }) {
40864
42103
  return /* @__PURE__ */ jsxRuntimeExports.jsx(
40865
42104
  Header,
@@ -40884,7 +42123,9 @@ const MemoizedHeader = reactExports.memo(function MemoizedHeader2({
40884
42123
  onRedo,
40885
42124
  canUndo,
40886
42125
  canRedo,
40887
- onUnCorrectAll
42126
+ onUnCorrectAll,
42127
+ annotationsEnabled,
42128
+ onAnnotationsToggle
40888
42129
  }
40889
42130
  );
40890
42131
  });
@@ -40920,10 +42161,14 @@ function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly,
40920
42161
  const [annotations, setAnnotations] = reactExports.useState([]);
40921
42162
  const [isAnnotationModalOpen, setIsAnnotationModalOpen] = reactExports.useState(false);
40922
42163
  const [pendingAnnotation, setPendingAnnotation] = reactExports.useState(null);
40923
- const [annotationsEnabled] = reactExports.useState(() => {
42164
+ const [annotationsEnabled, setAnnotationsEnabled] = reactExports.useState(() => {
40924
42165
  const saved = localStorage.getItem("annotationsEnabled");
40925
42166
  return saved !== null ? saved === "true" : true;
40926
42167
  });
42168
+ const handleAnnotationsToggle = reactExports.useCallback((enabled) => {
42169
+ setAnnotationsEnabled(enabled);
42170
+ localStorage.setItem("annotationsEnabled", String(enabled));
42171
+ }, []);
40927
42172
  const [correctionDetailOpen, setCorrectionDetailOpen] = reactExports.useState(false);
40928
42173
  const [selectedCorrection, setSelectedCorrection] = reactExports.useState(null);
40929
42174
  const theme2 = useTheme();
@@ -41473,7 +42718,9 @@ function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly,
41473
42718
  onRedo: handleRedo,
41474
42719
  canUndo,
41475
42720
  canRedo,
41476
- onUnCorrectAll: handleUnCorrectAll
42721
+ onUnCorrectAll: handleUnCorrectAll,
42722
+ annotationsEnabled,
42723
+ onAnnotationsToggle: handleAnnotationsToggle
41477
42724
  }
41478
42725
  ),
41479
42726
  /* @__PURE__ */ jsxRuntimeExports.jsxs(Grid, { container: true, direction: isMobile ? "column" : "row", children: [
@@ -41620,7 +42867,8 @@ function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly,
41620
42867
  onSave: handleSaveReplaceAllLyrics,
41621
42868
  onPlaySegment: handlePlaySegment,
41622
42869
  currentTime: currentAudioTime,
41623
- setModalSpacebarHandler: handleSetModalSpacebarHandler
42870
+ setModalSpacebarHandler: handleSetModalSpacebarHandler,
42871
+ existingSegments: data.corrected_segments
41624
42872
  }
41625
42873
  ),
41626
42874
  pendingAnnotation && /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -41690,22 +42938,23 @@ function App() {
41690
42938
  const params = new URLSearchParams(window.location.search);
41691
42939
  const encodedApiUrl = params.get("baseApiUrl");
41692
42940
  const audioHashParam = params.get("audioHash");
42941
+ const reviewTokenParam = params.get("reviewToken");
41693
42942
  if (encodedApiUrl) {
41694
42943
  const baseApiUrl = decodeURIComponent(encodedApiUrl);
41695
- setApiClient(new LiveApiClient(baseApiUrl));
42944
+ setApiClient(new LiveApiClient(baseApiUrl, reviewTokenParam || void 0));
41696
42945
  setIsReadOnly(false);
41697
42946
  if (audioHashParam) {
41698
42947
  setAudioHash(audioHashParam);
41699
42948
  }
41700
- fetchData(baseApiUrl);
42949
+ fetchData(baseApiUrl, reviewTokenParam || void 0);
41701
42950
  } else {
41702
42951
  setApiClient(new FileOnlyClient());
41703
42952
  setIsReadOnly(true);
41704
42953
  }
41705
42954
  }, []);
41706
- const fetchData = async (baseUrl) => {
42955
+ const fetchData = async (baseUrl, reviewToken) => {
41707
42956
  try {
41708
- const client2 = new LiveApiClient(baseUrl);
42957
+ const client2 = new LiveApiClient(baseUrl, reviewToken);
41709
42958
  const data2 = await client2.getCorrectionData();
41710
42959
  setData(data2);
41711
42960
  } catch (err) {
@@ -42025,7 +43274,7 @@ const theme = createTheme({
42025
43274
  spacing: (factor) => `${0.6 * factor}rem`
42026
43275
  // Further reduced from 0.8 * factor
42027
43276
  });
42028
- const version = "0.80.0";
43277
+ const version = "0.83.0";
42029
43278
  const packageJson = {
42030
43279
  version
42031
43280
  };
@@ -42036,4 +43285,4 @@ ReactDOM$1.createRoot(document.getElementById("root")).render(
42036
43285
  /* @__PURE__ */ jsxRuntimeExports.jsx(App, {})
42037
43286
  ] })
42038
43287
  );
42039
- //# sourceMappingURL=index-DdJTDWH3.js.map
43288
+ //# sourceMappingURL=index-BECn1o8Q.js.map