lyrics-transcriber 0.47.0__py3-none-any.whl → 0.49.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -33650,6 +33650,243 @@ function SegmentDetailsModal({
33650
33650
  const PlayCircleOutlineIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
33651
33651
  d: "m10 16.5 6-4.5-6-4.5zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2m0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8"
33652
33652
  }), "PlayCircleOutline");
33653
+ const DeleteOutlineIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
33654
+ d: "M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6zM8 9h8v10H8zm7.5-5-1-1h-5l-1 1H5v2h14V4z"
33655
+ }), "DeleteOutline");
33656
+ const urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
33657
+ let nanoid = (size = 21) => {
33658
+ let id = "";
33659
+ let bytes = crypto.getRandomValues(new Uint8Array(size |= 0));
33660
+ while (size--) {
33661
+ id += urlAlphabet[bytes[size] & 63];
33662
+ }
33663
+ return id;
33664
+ };
33665
+ const addSegmentBefore = (data, beforeIndex) => {
33666
+ const newData = { ...data };
33667
+ const beforeSegment = newData.corrected_segments[beforeIndex];
33668
+ const newStartTime = Math.max(0, (beforeSegment.start_time ?? 1) - 1);
33669
+ const newEndTime = newStartTime + 1;
33670
+ const newSegment = {
33671
+ id: nanoid(),
33672
+ text: "REPLACE",
33673
+ start_time: newStartTime,
33674
+ end_time: newEndTime,
33675
+ words: [{
33676
+ id: nanoid(),
33677
+ text: "REPLACE",
33678
+ start_time: newStartTime,
33679
+ end_time: newEndTime,
33680
+ confidence: 1
33681
+ }]
33682
+ };
33683
+ newData.corrected_segments.splice(beforeIndex, 0, newSegment);
33684
+ return newData;
33685
+ };
33686
+ const splitSegment = (data, segmentIndex, afterWordIndex) => {
33687
+ const newData = { ...data };
33688
+ const segment = newData.corrected_segments[segmentIndex];
33689
+ const firstHalfWords = segment.words.slice(0, afterWordIndex + 1);
33690
+ const secondHalfWords = segment.words.slice(afterWordIndex + 1);
33691
+ if (secondHalfWords.length === 0) return null;
33692
+ const lastFirstWord = firstHalfWords[firstHalfWords.length - 1];
33693
+ const firstSecondWord = secondHalfWords[0];
33694
+ const lastSecondWord = secondHalfWords[secondHalfWords.length - 1];
33695
+ const firstSegment = {
33696
+ ...segment,
33697
+ words: firstHalfWords,
33698
+ text: firstHalfWords.map((w) => w.text).join(" "),
33699
+ end_time: lastFirstWord.end_time ?? null
33700
+ };
33701
+ const secondSegment = {
33702
+ id: nanoid(),
33703
+ words: secondHalfWords,
33704
+ text: secondHalfWords.map((w) => w.text).join(" "),
33705
+ start_time: firstSecondWord.start_time ?? null,
33706
+ end_time: lastSecondWord.end_time ?? null
33707
+ };
33708
+ newData.corrected_segments.splice(segmentIndex, 1, firstSegment, secondSegment);
33709
+ return newData;
33710
+ };
33711
+ const deleteSegment = (data, segmentIndex) => {
33712
+ const newData = { ...data };
33713
+ const deletedSegment = newData.corrected_segments[segmentIndex];
33714
+ newData.corrected_segments = newData.corrected_segments.filter((_, index) => index !== segmentIndex);
33715
+ newData.anchor_sequences = newData.anchor_sequences.map((anchor) => ({
33716
+ ...anchor,
33717
+ transcribed_word_ids: anchor.transcribed_word_ids.filter(
33718
+ (wordId) => !deletedSegment.words.some((deletedWord) => deletedWord.id === wordId)
33719
+ )
33720
+ }));
33721
+ newData.gap_sequences = newData.gap_sequences.map((gap2) => ({
33722
+ ...gap2,
33723
+ transcribed_word_ids: gap2.transcribed_word_ids.filter(
33724
+ (wordId) => !deletedSegment.words.some((deletedWord) => deletedWord.id === wordId)
33725
+ )
33726
+ }));
33727
+ return newData;
33728
+ };
33729
+ const updateSegment = (data, segmentIndex, updatedSegment) => {
33730
+ const newData = { ...data };
33731
+ updatedSegment.words = updatedSegment.words.map((word) => ({
33732
+ ...word,
33733
+ id: word.id || nanoid()
33734
+ }));
33735
+ newData.corrected_segments[segmentIndex] = updatedSegment;
33736
+ return newData;
33737
+ };
33738
+ function mergeSegment(data, segmentIndex, mergeWithNext) {
33739
+ const segments = [...data.corrected_segments];
33740
+ const targetIndex = mergeWithNext ? segmentIndex + 1 : segmentIndex - 1;
33741
+ if (targetIndex < 0 || targetIndex >= segments.length) {
33742
+ return data;
33743
+ }
33744
+ const baseSegment = segments[segmentIndex];
33745
+ const targetSegment = segments[targetIndex];
33746
+ const mergedSegment = {
33747
+ id: nanoid(),
33748
+ words: mergeWithNext ? [...baseSegment.words, ...targetSegment.words] : [...targetSegment.words, ...baseSegment.words],
33749
+ text: mergeWithNext ? `${baseSegment.text} ${targetSegment.text}` : `${targetSegment.text} ${baseSegment.text}`,
33750
+ start_time: Math.min(
33751
+ baseSegment.start_time ?? Infinity,
33752
+ targetSegment.start_time ?? Infinity
33753
+ ),
33754
+ end_time: Math.max(
33755
+ baseSegment.end_time ?? -Infinity,
33756
+ targetSegment.end_time ?? -Infinity
33757
+ )
33758
+ };
33759
+ const minIndex = Math.min(segmentIndex, targetIndex);
33760
+ segments.splice(minIndex, 2, mergedSegment);
33761
+ return {
33762
+ ...data,
33763
+ corrected_segments: segments
33764
+ };
33765
+ }
33766
+ function findAndReplace(data, findText, replaceText, options = {
33767
+ caseSensitive: false,
33768
+ useRegex: false,
33769
+ fullTextMode: false
33770
+ }) {
33771
+ const newData = { ...data };
33772
+ if (options.fullTextMode) {
33773
+ newData.corrected_segments = data.corrected_segments.map((segment) => {
33774
+ let pattern;
33775
+ if (options.useRegex) {
33776
+ pattern = new RegExp(findText, options.caseSensitive ? "g" : "gi");
33777
+ } else {
33778
+ const escapedFindText = findText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
33779
+ pattern = new RegExp(escapedFindText, options.caseSensitive ? "g" : "gi");
33780
+ }
33781
+ const segmentText = segment.text;
33782
+ if (!pattern.test(segmentText)) {
33783
+ return segment;
33784
+ }
33785
+ pattern.lastIndex = 0;
33786
+ const newSegmentText = segmentText.replace(pattern, replaceText);
33787
+ const newWordTexts = newSegmentText.trim().split(/\s+/).filter((text) => text.length > 0);
33788
+ const newWords = [];
33789
+ if (newWordTexts.length === segment.words.length) {
33790
+ for (let i = 0; i < newWordTexts.length; i++) {
33791
+ newWords.push({
33792
+ ...segment.words[i],
33793
+ text: newWordTexts[i]
33794
+ });
33795
+ }
33796
+ } else if (newWordTexts.length < segment.words.length) {
33797
+ let oldWordIndex = 0;
33798
+ for (let i = 0; i < newWordTexts.length; i++) {
33799
+ while (oldWordIndex < segment.words.length && segment.words[oldWordIndex].text.trim() === "") {
33800
+ oldWordIndex++;
33801
+ }
33802
+ if (oldWordIndex < segment.words.length) {
33803
+ newWords.push({
33804
+ ...segment.words[oldWordIndex],
33805
+ text: newWordTexts[i]
33806
+ });
33807
+ oldWordIndex++;
33808
+ } else {
33809
+ newWords.push({
33810
+ id: nanoid(),
33811
+ text: newWordTexts[i],
33812
+ start_time: null,
33813
+ end_time: null
33814
+ });
33815
+ }
33816
+ }
33817
+ } else {
33818
+ for (let i = 0; i < newWordTexts.length; i++) {
33819
+ if (i < segment.words.length) {
33820
+ newWords.push({
33821
+ ...segment.words[i],
33822
+ text: newWordTexts[i]
33823
+ });
33824
+ } else {
33825
+ newWords.push({
33826
+ id: nanoid(),
33827
+ text: newWordTexts[i],
33828
+ start_time: null,
33829
+ end_time: null
33830
+ });
33831
+ }
33832
+ }
33833
+ }
33834
+ return {
33835
+ ...segment,
33836
+ words: newWords,
33837
+ text: newSegmentText
33838
+ };
33839
+ });
33840
+ } else {
33841
+ newData.corrected_segments = data.corrected_segments.map((segment) => {
33842
+ let newWords = segment.words.map((word) => {
33843
+ let pattern;
33844
+ if (options.useRegex) {
33845
+ pattern = new RegExp(findText, options.caseSensitive ? "g" : "gi");
33846
+ } else {
33847
+ const escapedFindText = findText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
33848
+ pattern = new RegExp(escapedFindText, options.caseSensitive ? "g" : "gi");
33849
+ }
33850
+ return {
33851
+ ...word,
33852
+ text: word.text.replace(pattern, replaceText)
33853
+ };
33854
+ });
33855
+ newWords = newWords.filter((word) => word.text.trim() !== "");
33856
+ return {
33857
+ ...segment,
33858
+ words: newWords,
33859
+ text: newWords.map((w) => w.text).join(" ")
33860
+ };
33861
+ });
33862
+ }
33863
+ newData.corrected_segments = newData.corrected_segments.filter((segment) => segment.words.length > 0);
33864
+ return newData;
33865
+ }
33866
+ function deleteWord(data, wordId) {
33867
+ const segmentIndex = data.corrected_segments.findIndex(
33868
+ (segment2) => segment2.words.some((word) => word.id === wordId)
33869
+ );
33870
+ if (segmentIndex === -1) {
33871
+ return data;
33872
+ }
33873
+ const segment = data.corrected_segments[segmentIndex];
33874
+ const wordIndex = segment.words.findIndex((word) => word.id === wordId);
33875
+ if (wordIndex === -1) {
33876
+ return data;
33877
+ }
33878
+ const updatedWords = segment.words.filter((_, index) => index !== wordIndex);
33879
+ if (updatedWords.length > 0) {
33880
+ const updatedSegment = {
33881
+ ...segment,
33882
+ words: updatedWords,
33883
+ text: updatedWords.map((w) => w.text).join(" ")
33884
+ };
33885
+ return updateSegment(data, segmentIndex, updatedSegment);
33886
+ } else {
33887
+ return deleteSegment(data, segmentIndex);
33888
+ }
33889
+ }
33653
33890
  const SegmentIndex = styled(Typography)(({ theme: theme2 }) => ({
33654
33891
  color: theme2.palette.text.secondary,
33655
33892
  width: "1.8em",
@@ -33688,9 +33925,16 @@ function TranscriptionView({
33688
33925
  mode,
33689
33926
  onPlaySegment,
33690
33927
  currentTime = 0,
33691
- anchors = []
33928
+ anchors = [],
33929
+ onDataChange
33692
33930
  }) {
33693
33931
  const [selectedSegmentIndex, setSelectedSegmentIndex] = reactExports.useState(null);
33932
+ const handleDeleteSegment = (segmentIndex) => {
33933
+ if (onDataChange) {
33934
+ const updatedData = deleteSegment(data, segmentIndex);
33935
+ onDataChange(updatedData);
33936
+ }
33937
+ };
33694
33938
  return /* @__PURE__ */ jsxRuntimeExports.jsxs(Paper, { sx: { p: 0.8 }, children: [
33695
33939
  /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { sx: { display: "flex", justifyContent: "space-between", alignItems: "center", mb: 0.5 }, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "h6", sx: { fontSize: "0.9rem", mb: 0 }, children: "Corrected Transcription" }) }),
33696
33940
  /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { sx: { display: "flex", flexDirection: "column", gap: 0.2 }, children: data.corrected_segments.map((segment, segmentIndex) => {
@@ -33745,6 +33989,22 @@ function TranscriptionView({
33745
33989
  children: segmentIndex
33746
33990
  }
33747
33991
  ),
33992
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
33993
+ IconButton,
33994
+ {
33995
+ size: "small",
33996
+ onClick: () => handleDeleteSegment(segmentIndex),
33997
+ sx: {
33998
+ padding: "1px",
33999
+ height: "18px",
34000
+ width: "18px",
34001
+ minHeight: "18px",
34002
+ minWidth: "18px"
34003
+ },
34004
+ title: "Delete segment",
34005
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(DeleteOutlineIcon, { sx: { fontSize: "0.9rem", color: "error.main" } })
34006
+ }
34007
+ ),
33748
34008
  segment.start_time !== null && /* @__PURE__ */ jsxRuntimeExports.jsx(
33749
34009
  IconButton,
33750
34010
  {
@@ -33757,6 +34017,7 @@ function TranscriptionView({
33757
34017
  minHeight: "18px",
33758
34018
  minWidth: "18px"
33759
34019
  },
34020
+ title: "Play segment",
33760
34021
  children: /* @__PURE__ */ jsxRuntimeExports.jsx(PlayCircleOutlineIcon, { sx: { fontSize: "0.9rem" } })
33761
34022
  }
33762
34023
  )
@@ -33794,15 +34055,6 @@ function TranscriptionView({
33794
34055
  const StopIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
33795
34056
  d: "M6 6h12v12H6z"
33796
34057
  }), "Stop");
33797
- const urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
33798
- let nanoid = (size = 21) => {
33799
- let id = "";
33800
- let bytes = crypto.getRandomValues(new Uint8Array(size |= 0));
33801
- while (size--) {
33802
- id += urlAlphabet[bytes[size] & 63];
33803
- }
33804
- return id;
33805
- };
33806
34058
  const TAP_THRESHOLD_MS = 200;
33807
34059
  const DEFAULT_WORD_DURATION = 1;
33808
34060
  const OVERLAP_BUFFER = 0.01;
@@ -35710,6 +35962,12 @@ const PauseIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
35710
35962
  const PlayArrowIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
35711
35963
  d: "M8 5v14l11-7z"
35712
35964
  }), "PlayArrow");
35965
+ const RedoIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
35966
+ 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"
35967
+ }), "Redo");
35968
+ const UndoIcon = createSvgIcon(/* @__PURE__ */ jsxRuntimeExports.jsx("path", {
35969
+ d: "M12.5 8c-2.65 0-5.05.99-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88 3.54 0 6.55 2.31 7.6 5.5l2.37-.78C21.08 11.03 17.15 8 12.5 8"
35970
+ }), "Undo");
35713
35971
  const normalizeWordForComparison = (word) => ({
35714
35972
  text: word.text,
35715
35973
  start_time: word.start_time ?? 0,
@@ -35787,410 +36045,185 @@ function ReviewChangesModal({
35787
36045
  path: `Word ${wordIndex}`,
35788
36046
  oldValue: `"${word.text}" (${((_a3 = word.start_time) == null ? void 0 : _a3.toFixed(4)) ?? "N/A"} - ${((_b3 = word.end_time) == null ? void 0 : _b3.toFixed(4)) ?? "N/A"})`
35789
36047
  });
35790
- return;
35791
- }
35792
- if (word.text !== updatedWord.text || Math.abs((word.start_time ?? 0) - (updatedWord.start_time ?? 0)) > 1e-4 || Math.abs((word.end_time ?? 0) - (updatedWord.end_time ?? 0)) > 1e-4) {
35793
- wordChanges.push({
35794
- type: "modified",
35795
- path: `Word ${wordIndex}`,
35796
- oldValue: `"${word.text}" (${((_c2 = word.start_time) == null ? void 0 : _c2.toFixed(4)) ?? "N/A"} - ${((_d2 = word.end_time) == null ? void 0 : _d2.toFixed(4)) ?? "N/A"})`,
35797
- newValue: `"${updatedWord.text}" (${((_e2 = updatedWord.start_time) == null ? void 0 : _e2.toFixed(4)) ?? "N/A"} - ${((_f2 = updatedWord.end_time) == null ? void 0 : _f2.toFixed(4)) ?? "N/A"})`
35798
- });
35799
- }
35800
- });
35801
- if (normalizedUpdated.words.length > normalizedOriginal.words.length) {
35802
- for (let i = normalizedOriginal.words.length; i < normalizedUpdated.words.length; i++) {
35803
- const word = normalizedUpdated.words[i];
35804
- wordChanges.push({
35805
- type: "added",
35806
- path: `Word ${i}`,
35807
- newValue: `"${word.text}" (${((_a2 = word.start_time) == null ? void 0 : _a2.toFixed(4)) ?? "N/A"} - ${((_b2 = word.end_time) == null ? void 0 : _b2.toFixed(4)) ?? "N/A"})`
35808
- });
35809
- }
35810
- }
35811
- if (normalizedOriginal.text !== normalizedUpdated.text || Math.abs((normalizedOriginal.start_time ?? 0) - (normalizedUpdated.start_time ?? 0)) > 1e-4 || Math.abs((normalizedOriginal.end_time ?? 0) - (normalizedUpdated.end_time ?? 0)) > 1e-4 || wordChanges.length > 0) {
35812
- diffs.push({
35813
- type: "modified",
35814
- path: `Segment ${index}`,
35815
- segmentIndex: index,
35816
- oldValue: `"${normalizedOriginal.text}" (${((_c = normalizedOriginal.start_time) == null ? void 0 : _c.toFixed(4)) ?? "N/A"} - ${((_d = normalizedOriginal.end_time) == null ? void 0 : _d.toFixed(4)) ?? "N/A"})`,
35817
- newValue: `"${normalizedUpdated.text}" (${((_e = normalizedUpdated.start_time) == null ? void 0 : _e.toFixed(4)) ?? "N/A"} - ${((_f = normalizedUpdated.end_time) == null ? void 0 : _f.toFixed(4)) ?? "N/A"})`,
35818
- wordChanges: wordChanges.length > 0 ? wordChanges : void 0
35819
- });
35820
- }
35821
- });
35822
- if (updatedData.corrected_segments.length > originalData.corrected_segments.length) {
35823
- for (let i = originalData.corrected_segments.length; i < updatedData.corrected_segments.length; i++) {
35824
- const segment = updatedData.corrected_segments[i];
35825
- diffs.push({
35826
- type: "added",
35827
- path: `Segment ${i}`,
35828
- segmentIndex: i,
35829
- newValue: `"${segment.text}" (${((_a = segment.start_time) == null ? void 0 : _a.toFixed(4)) ?? "N/A"} - ${((_b = segment.end_time) == null ? void 0 : _b.toFixed(4)) ?? "N/A"})`
35830
- });
35831
- }
35832
- }
35833
- return diffs;
35834
- }, [originalData, updatedData]);
35835
- const renderCompactDiff = (diff) => {
35836
- var _a, _b, _c;
35837
- if (diff.type !== "modified") {
35838
- return /* @__PURE__ */ jsxRuntimeExports.jsxs(
35839
- Typography,
35840
- {
35841
- color: diff.type === "added" ? "success.main" : "error.main",
35842
- sx: { mb: 0.5 },
35843
- children: [
35844
- diff.segmentIndex,
35845
- ": ",
35846
- diff.type === "added" ? "+ " : "- ",
35847
- diff.type === "added" ? diff.newValue : diff.oldValue
35848
- ]
35849
- },
35850
- diff.path
35851
- );
35852
- }
35853
- const oldText = ((_a = diff.oldValue) == null ? void 0 : _a.split('"')[1]) || "";
35854
- const newText = ((_b = diff.newValue) == null ? void 0 : _b.split('"')[1]) || "";
35855
- const oldWords = oldText.split(" ");
35856
- const newWords = newText.split(" ");
35857
- const timingMatch = (_c = diff.newValue) == null ? void 0 : _c.match(/\(([\d.]+) - ([\d.]+)\)/);
35858
- const timing = timingMatch ? `(${parseFloat(timingMatch[1]).toFixed(2)} - ${parseFloat(timingMatch[2]).toFixed(2)})` : "";
35859
- const unifiedDiff = [];
35860
- let i = 0, j = 0;
35861
- while (i < oldWords.length || j < newWords.length) {
35862
- if (i < oldWords.length && j < newWords.length && oldWords[i] === newWords[j]) {
35863
- unifiedDiff.push({ type: "unchanged", text: oldWords[i] });
35864
- i++;
35865
- j++;
35866
- } else if (i < oldWords.length && (!newWords[j] || oldWords[i] !== newWords[j])) {
35867
- unifiedDiff.push({ type: "deleted", text: oldWords[i] });
35868
- i++;
35869
- } else if (j < newWords.length) {
35870
- unifiedDiff.push({ type: "added", text: newWords[j] });
35871
- j++;
35872
- }
35873
- }
35874
- return /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { mb: 0.5, display: "flex", alignItems: "center" }, children: [
35875
- /* @__PURE__ */ jsxRuntimeExports.jsxs(Typography, { variant: "body2", color: "text.secondary", sx: { mr: 1, minWidth: "30px" }, children: [
35876
- diff.segmentIndex,
35877
- ":"
35878
- ] }),
35879
- /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { display: "flex", flexWrap: "wrap", flexGrow: 1, alignItems: "center" }, children: [
35880
- unifiedDiff.map((word, idx) => /* @__PURE__ */ jsxRuntimeExports.jsx(
35881
- Typography,
35882
- {
35883
- component: "span",
35884
- color: word.type === "unchanged" ? "text.primary" : word.type === "deleted" ? "error.main" : "success.main",
35885
- sx: {
35886
- textDecoration: word.type === "deleted" ? "line-through" : "none",
35887
- mr: 0.5
35888
- },
35889
- children: word.text
35890
- },
35891
- idx
35892
- )),
35893
- /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "body2", color: "text.secondary", sx: { ml: 1 }, children: timing })
35894
- ] })
35895
- ] }, diff.path);
35896
- };
35897
- return /* @__PURE__ */ jsxRuntimeExports.jsxs(
35898
- Dialog,
35899
- {
35900
- open,
35901
- onClose,
35902
- maxWidth: "md",
35903
- fullWidth: true,
35904
- children: [
35905
- /* @__PURE__ */ jsxRuntimeExports.jsx(DialogTitle, { children: "Preview Video (With Vocals)" }),
35906
- /* @__PURE__ */ jsxRuntimeExports.jsxs(
35907
- DialogContent,
35908
- {
35909
- dividers: true,
35910
- sx: {
35911
- p: 0,
35912
- // Remove default padding
35913
- "&:first-of-type": { pt: 0 }
35914
- // Remove default top padding
35915
- },
35916
- children: [
35917
- /* @__PURE__ */ jsxRuntimeExports.jsx(
35918
- PreviewVideoSection,
35919
- {
35920
- apiClient,
35921
- isModalOpen: open,
35922
- updatedData,
35923
- videoRef
35924
- }
35925
- ),
35926
- /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { sx: { p: 2, mt: 0 }, children: differences.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { children: [
35927
- /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { color: "text.secondary", children: "No manual corrections detected. If everything looks good in the preview, click submit and the server will generate the final karaoke video." }),
35928
- /* @__PURE__ */ jsxRuntimeExports.jsxs(Typography, { variant: "body2", color: "text.secondary", children: [
35929
- "Total segments: ",
35930
- updatedData.corrected_segments.length
35931
- ] })
35932
- ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { children: [
35933
- /* @__PURE__ */ jsxRuntimeExports.jsxs(Typography, { variant: "body2", color: "text.secondary", sx: { mb: 1 }, children: [
35934
- differences.length,
35935
- " segment",
35936
- differences.length !== 1 ? "s" : "",
35937
- " modified:"
35938
- ] }),
35939
- /* @__PURE__ */ jsxRuntimeExports.jsx(Paper, { sx: { p: 2 }, children: differences.map(renderCompactDiff) })
35940
- ] }) })
35941
- ]
35942
- }
35943
- ),
35944
- /* @__PURE__ */ jsxRuntimeExports.jsxs(DialogActions, { children: [
35945
- /* @__PURE__ */ jsxRuntimeExports.jsx(
35946
- Button,
35947
- {
35948
- onClick: onClose,
35949
- color: "warning",
35950
- startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowBack, {}),
35951
- sx: { mr: "auto" },
35952
- children: "Cancel"
35953
- }
35954
- ),
35955
- /* @__PURE__ */ jsxRuntimeExports.jsx(
35956
- Button,
35957
- {
35958
- onClick: onSubmit,
35959
- variant: "contained",
35960
- color: "success",
35961
- endIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(CloudUpload, {}),
35962
- children: "Complete Review"
35963
- }
35964
- )
35965
- ] })
35966
- ]
35967
- }
35968
- );
35969
- }
35970
- const addSegmentBefore = (data, beforeIndex) => {
35971
- const newData = { ...data };
35972
- const beforeSegment = newData.corrected_segments[beforeIndex];
35973
- const newStartTime = Math.max(0, (beforeSegment.start_time ?? 1) - 1);
35974
- const newEndTime = newStartTime + 1;
35975
- const newSegment = {
35976
- id: nanoid(),
35977
- text: "REPLACE",
35978
- start_time: newStartTime,
35979
- end_time: newEndTime,
35980
- words: [{
35981
- id: nanoid(),
35982
- text: "REPLACE",
35983
- start_time: newStartTime,
35984
- end_time: newEndTime,
35985
- confidence: 1
35986
- }]
35987
- };
35988
- newData.corrected_segments.splice(beforeIndex, 0, newSegment);
35989
- return newData;
35990
- };
35991
- const splitSegment = (data, segmentIndex, afterWordIndex) => {
35992
- const newData = { ...data };
35993
- const segment = newData.corrected_segments[segmentIndex];
35994
- const firstHalfWords = segment.words.slice(0, afterWordIndex + 1);
35995
- const secondHalfWords = segment.words.slice(afterWordIndex + 1);
35996
- if (secondHalfWords.length === 0) return null;
35997
- const lastFirstWord = firstHalfWords[firstHalfWords.length - 1];
35998
- const firstSecondWord = secondHalfWords[0];
35999
- const lastSecondWord = secondHalfWords[secondHalfWords.length - 1];
36000
- const firstSegment = {
36001
- ...segment,
36002
- words: firstHalfWords,
36003
- text: firstHalfWords.map((w) => w.text).join(" "),
36004
- end_time: lastFirstWord.end_time ?? null
36005
- };
36006
- const secondSegment = {
36007
- id: nanoid(),
36008
- words: secondHalfWords,
36009
- text: secondHalfWords.map((w) => w.text).join(" "),
36010
- start_time: firstSecondWord.start_time ?? null,
36011
- end_time: lastSecondWord.end_time ?? null
36012
- };
36013
- newData.corrected_segments.splice(segmentIndex, 1, firstSegment, secondSegment);
36014
- return newData;
36015
- };
36016
- const deleteSegment = (data, segmentIndex) => {
36017
- const newData = { ...data };
36018
- const deletedSegment = newData.corrected_segments[segmentIndex];
36019
- newData.corrected_segments = newData.corrected_segments.filter((_, index) => index !== segmentIndex);
36020
- newData.anchor_sequences = newData.anchor_sequences.map((anchor) => ({
36021
- ...anchor,
36022
- transcribed_word_ids: anchor.transcribed_word_ids.filter(
36023
- (wordId) => !deletedSegment.words.some((deletedWord) => deletedWord.id === wordId)
36024
- )
36025
- }));
36026
- newData.gap_sequences = newData.gap_sequences.map((gap2) => ({
36027
- ...gap2,
36028
- transcribed_word_ids: gap2.transcribed_word_ids.filter(
36029
- (wordId) => !deletedSegment.words.some((deletedWord) => deletedWord.id === wordId)
36030
- )
36031
- }));
36032
- return newData;
36033
- };
36034
- const updateSegment = (data, segmentIndex, updatedSegment) => {
36035
- const newData = { ...data };
36036
- updatedSegment.words = updatedSegment.words.map((word) => ({
36037
- ...word,
36038
- id: word.id || nanoid()
36039
- }));
36040
- newData.corrected_segments[segmentIndex] = updatedSegment;
36041
- return newData;
36042
- };
36043
- function mergeSegment(data, segmentIndex, mergeWithNext) {
36044
- const segments = [...data.corrected_segments];
36045
- const targetIndex = mergeWithNext ? segmentIndex + 1 : segmentIndex - 1;
36046
- if (targetIndex < 0 || targetIndex >= segments.length) {
36047
- return data;
36048
- }
36049
- const baseSegment = segments[segmentIndex];
36050
- const targetSegment = segments[targetIndex];
36051
- const mergedSegment = {
36052
- id: nanoid(),
36053
- words: mergeWithNext ? [...baseSegment.words, ...targetSegment.words] : [...targetSegment.words, ...baseSegment.words],
36054
- text: mergeWithNext ? `${baseSegment.text} ${targetSegment.text}` : `${targetSegment.text} ${baseSegment.text}`,
36055
- start_time: Math.min(
36056
- baseSegment.start_time ?? Infinity,
36057
- targetSegment.start_time ?? Infinity
36058
- ),
36059
- end_time: Math.max(
36060
- baseSegment.end_time ?? -Infinity,
36061
- targetSegment.end_time ?? -Infinity
36062
- )
36063
- };
36064
- const minIndex = Math.min(segmentIndex, targetIndex);
36065
- segments.splice(minIndex, 2, mergedSegment);
36066
- return {
36067
- ...data,
36068
- corrected_segments: segments
36069
- };
36070
- }
36071
- function findAndReplace(data, findText, replaceText, options = {
36072
- caseSensitive: false,
36073
- useRegex: false,
36074
- fullTextMode: false
36075
- }) {
36076
- const newData = { ...data };
36077
- if (options.fullTextMode) {
36078
- newData.corrected_segments = data.corrected_segments.map((segment) => {
36079
- let pattern;
36080
- if (options.useRegex) {
36081
- pattern = new RegExp(findText, options.caseSensitive ? "g" : "gi");
36082
- } else {
36083
- const escapedFindText = findText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
36084
- pattern = new RegExp(escapedFindText, options.caseSensitive ? "g" : "gi");
36085
- }
36086
- const segmentText = segment.text;
36087
- if (!pattern.test(segmentText)) {
36088
- return segment;
36089
- }
36090
- pattern.lastIndex = 0;
36091
- const newSegmentText = segmentText.replace(pattern, replaceText);
36092
- const newWordTexts = newSegmentText.trim().split(/\s+/).filter((text) => text.length > 0);
36093
- const newWords = [];
36094
- if (newWordTexts.length === segment.words.length) {
36095
- for (let i = 0; i < newWordTexts.length; i++) {
36096
- newWords.push({
36097
- ...segment.words[i],
36098
- text: newWordTexts[i]
36099
- });
36100
- }
36101
- } else if (newWordTexts.length < segment.words.length) {
36102
- let oldWordIndex = 0;
36103
- for (let i = 0; i < newWordTexts.length; i++) {
36104
- while (oldWordIndex < segment.words.length && segment.words[oldWordIndex].text.trim() === "") {
36105
- oldWordIndex++;
36106
- }
36107
- if (oldWordIndex < segment.words.length) {
36108
- newWords.push({
36109
- ...segment.words[oldWordIndex],
36110
- text: newWordTexts[i]
36111
- });
36112
- oldWordIndex++;
36113
- } else {
36114
- newWords.push({
36115
- id: nanoid(),
36116
- text: newWordTexts[i],
36117
- start_time: null,
36118
- end_time: null
36119
- });
36120
- }
36121
- }
36122
- } else {
36123
- for (let i = 0; i < newWordTexts.length; i++) {
36124
- if (i < segment.words.length) {
36125
- newWords.push({
36126
- ...segment.words[i],
36127
- text: newWordTexts[i]
36128
- });
36129
- } else {
36130
- newWords.push({
36131
- id: nanoid(),
36132
- text: newWordTexts[i],
36133
- start_time: null,
36134
- end_time: null
36135
- });
36136
- }
36137
- }
36138
- }
36139
- return {
36140
- ...segment,
36141
- words: newWords,
36142
- text: newSegmentText
36143
- };
36144
- });
36145
- } else {
36146
- newData.corrected_segments = data.corrected_segments.map((segment) => {
36147
- let newWords = segment.words.map((word) => {
36148
- let pattern;
36149
- if (options.useRegex) {
36150
- pattern = new RegExp(findText, options.caseSensitive ? "g" : "gi");
36151
- } else {
36152
- const escapedFindText = findText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
36153
- pattern = new RegExp(escapedFindText, options.caseSensitive ? "g" : "gi");
36048
+ return;
36049
+ }
36050
+ if (word.text !== updatedWord.text || Math.abs((word.start_time ?? 0) - (updatedWord.start_time ?? 0)) > 1e-4 || Math.abs((word.end_time ?? 0) - (updatedWord.end_time ?? 0)) > 1e-4) {
36051
+ wordChanges.push({
36052
+ type: "modified",
36053
+ path: `Word ${wordIndex}`,
36054
+ oldValue: `"${word.text}" (${((_c2 = word.start_time) == null ? void 0 : _c2.toFixed(4)) ?? "N/A"} - ${((_d2 = word.end_time) == null ? void 0 : _d2.toFixed(4)) ?? "N/A"})`,
36055
+ newValue: `"${updatedWord.text}" (${((_e2 = updatedWord.start_time) == null ? void 0 : _e2.toFixed(4)) ?? "N/A"} - ${((_f2 = updatedWord.end_time) == null ? void 0 : _f2.toFixed(4)) ?? "N/A"})`
36056
+ });
36154
36057
  }
36155
- return {
36156
- ...word,
36157
- text: word.text.replace(pattern, replaceText)
36158
- };
36159
36058
  });
36160
- newWords = newWords.filter((word) => word.text.trim() !== "");
36161
- return {
36162
- ...segment,
36163
- words: newWords,
36164
- text: newWords.map((w) => w.text).join(" ")
36165
- };
36059
+ if (normalizedUpdated.words.length > normalizedOriginal.words.length) {
36060
+ for (let i = normalizedOriginal.words.length; i < normalizedUpdated.words.length; i++) {
36061
+ const word = normalizedUpdated.words[i];
36062
+ wordChanges.push({
36063
+ type: "added",
36064
+ path: `Word ${i}`,
36065
+ newValue: `"${word.text}" (${((_a2 = word.start_time) == null ? void 0 : _a2.toFixed(4)) ?? "N/A"} - ${((_b2 = word.end_time) == null ? void 0 : _b2.toFixed(4)) ?? "N/A"})`
36066
+ });
36067
+ }
36068
+ }
36069
+ if (normalizedOriginal.text !== normalizedUpdated.text || Math.abs((normalizedOriginal.start_time ?? 0) - (normalizedUpdated.start_time ?? 0)) > 1e-4 || Math.abs((normalizedOriginal.end_time ?? 0) - (normalizedUpdated.end_time ?? 0)) > 1e-4 || wordChanges.length > 0) {
36070
+ diffs.push({
36071
+ type: "modified",
36072
+ path: `Segment ${index}`,
36073
+ segmentIndex: index,
36074
+ oldValue: `"${normalizedOriginal.text}" (${((_c = normalizedOriginal.start_time) == null ? void 0 : _c.toFixed(4)) ?? "N/A"} - ${((_d = normalizedOriginal.end_time) == null ? void 0 : _d.toFixed(4)) ?? "N/A"})`,
36075
+ newValue: `"${normalizedUpdated.text}" (${((_e = normalizedUpdated.start_time) == null ? void 0 : _e.toFixed(4)) ?? "N/A"} - ${((_f = normalizedUpdated.end_time) == null ? void 0 : _f.toFixed(4)) ?? "N/A"})`,
36076
+ wordChanges: wordChanges.length > 0 ? wordChanges : void 0
36077
+ });
36078
+ }
36166
36079
  });
36167
- }
36168
- newData.corrected_segments = newData.corrected_segments.filter((segment) => segment.words.length > 0);
36169
- return newData;
36170
- }
36171
- function deleteWord(data, wordId) {
36172
- const segmentIndex = data.corrected_segments.findIndex(
36173
- (segment2) => segment2.words.some((word) => word.id === wordId)
36080
+ if (updatedData.corrected_segments.length > originalData.corrected_segments.length) {
36081
+ for (let i = originalData.corrected_segments.length; i < updatedData.corrected_segments.length; i++) {
36082
+ const segment = updatedData.corrected_segments[i];
36083
+ diffs.push({
36084
+ type: "added",
36085
+ path: `Segment ${i}`,
36086
+ segmentIndex: i,
36087
+ newValue: `"${segment.text}" (${((_a = segment.start_time) == null ? void 0 : _a.toFixed(4)) ?? "N/A"} - ${((_b = segment.end_time) == null ? void 0 : _b.toFixed(4)) ?? "N/A"})`
36088
+ });
36089
+ }
36090
+ }
36091
+ return diffs;
36092
+ }, [originalData, updatedData]);
36093
+ const renderCompactDiff = (diff) => {
36094
+ var _a, _b, _c;
36095
+ if (diff.type !== "modified") {
36096
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
36097
+ Typography,
36098
+ {
36099
+ color: diff.type === "added" ? "success.main" : "error.main",
36100
+ sx: { mb: 0.5 },
36101
+ children: [
36102
+ diff.segmentIndex,
36103
+ ": ",
36104
+ diff.type === "added" ? "+ " : "- ",
36105
+ diff.type === "added" ? diff.newValue : diff.oldValue
36106
+ ]
36107
+ },
36108
+ diff.path
36109
+ );
36110
+ }
36111
+ const oldText = ((_a = diff.oldValue) == null ? void 0 : _a.split('"')[1]) || "";
36112
+ const newText = ((_b = diff.newValue) == null ? void 0 : _b.split('"')[1]) || "";
36113
+ const oldWords = oldText.split(" ");
36114
+ const newWords = newText.split(" ");
36115
+ const timingMatch = (_c = diff.newValue) == null ? void 0 : _c.match(/\(([\d.]+) - ([\d.]+)\)/);
36116
+ const timing = timingMatch ? `(${parseFloat(timingMatch[1]).toFixed(2)} - ${parseFloat(timingMatch[2]).toFixed(2)})` : "";
36117
+ const unifiedDiff = [];
36118
+ let i = 0, j = 0;
36119
+ while (i < oldWords.length || j < newWords.length) {
36120
+ if (i < oldWords.length && j < newWords.length && oldWords[i] === newWords[j]) {
36121
+ unifiedDiff.push({ type: "unchanged", text: oldWords[i] });
36122
+ i++;
36123
+ j++;
36124
+ } else if (i < oldWords.length && (!newWords[j] || oldWords[i] !== newWords[j])) {
36125
+ unifiedDiff.push({ type: "deleted", text: oldWords[i] });
36126
+ i++;
36127
+ } else if (j < newWords.length) {
36128
+ unifiedDiff.push({ type: "added", text: newWords[j] });
36129
+ j++;
36130
+ }
36131
+ }
36132
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { mb: 0.5, display: "flex", alignItems: "center" }, children: [
36133
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(Typography, { variant: "body2", color: "text.secondary", sx: { mr: 1, minWidth: "30px" }, children: [
36134
+ diff.segmentIndex,
36135
+ ":"
36136
+ ] }),
36137
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { display: "flex", flexWrap: "wrap", flexGrow: 1, alignItems: "center" }, children: [
36138
+ unifiedDiff.map((word, idx) => /* @__PURE__ */ jsxRuntimeExports.jsx(
36139
+ Typography,
36140
+ {
36141
+ component: "span",
36142
+ color: word.type === "unchanged" ? "text.primary" : word.type === "deleted" ? "error.main" : "success.main",
36143
+ sx: {
36144
+ textDecoration: word.type === "deleted" ? "line-through" : "none",
36145
+ mr: 0.5
36146
+ },
36147
+ children: word.text
36148
+ },
36149
+ idx
36150
+ )),
36151
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { variant: "body2", color: "text.secondary", sx: { ml: 1 }, children: timing })
36152
+ ] })
36153
+ ] }, diff.path);
36154
+ };
36155
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
36156
+ Dialog,
36157
+ {
36158
+ open,
36159
+ onClose,
36160
+ maxWidth: "md",
36161
+ fullWidth: true,
36162
+ children: [
36163
+ /* @__PURE__ */ jsxRuntimeExports.jsx(DialogTitle, { children: "Preview Video (With Vocals)" }),
36164
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
36165
+ DialogContent,
36166
+ {
36167
+ dividers: true,
36168
+ sx: {
36169
+ p: 0,
36170
+ // Remove default padding
36171
+ "&:first-of-type": { pt: 0 }
36172
+ // Remove default top padding
36173
+ },
36174
+ children: [
36175
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
36176
+ PreviewVideoSection,
36177
+ {
36178
+ apiClient,
36179
+ isModalOpen: open,
36180
+ updatedData,
36181
+ videoRef
36182
+ }
36183
+ ),
36184
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { sx: { p: 2, mt: 0 }, children: differences.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { children: [
36185
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Typography, { color: "text.secondary", children: "No manual corrections detected. If everything looks good in the preview, click submit and the server will generate the final karaoke video." }),
36186
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(Typography, { variant: "body2", color: "text.secondary", children: [
36187
+ "Total segments: ",
36188
+ updatedData.corrected_segments.length
36189
+ ] })
36190
+ ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { children: [
36191
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(Typography, { variant: "body2", color: "text.secondary", sx: { mb: 1 }, children: [
36192
+ differences.length,
36193
+ " segment",
36194
+ differences.length !== 1 ? "s" : "",
36195
+ " modified:"
36196
+ ] }),
36197
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Paper, { sx: { p: 2 }, children: differences.map(renderCompactDiff) })
36198
+ ] }) })
36199
+ ]
36200
+ }
36201
+ ),
36202
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(DialogActions, { children: [
36203
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
36204
+ Button,
36205
+ {
36206
+ onClick: onClose,
36207
+ color: "warning",
36208
+ startIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowBack, {}),
36209
+ sx: { mr: "auto" },
36210
+ children: "Cancel"
36211
+ }
36212
+ ),
36213
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
36214
+ Button,
36215
+ {
36216
+ onClick: onSubmit,
36217
+ variant: "contained",
36218
+ color: "success",
36219
+ endIcon: /* @__PURE__ */ jsxRuntimeExports.jsx(CloudUpload, {}),
36220
+ children: "Complete Review"
36221
+ }
36222
+ )
36223
+ ] })
36224
+ ]
36225
+ }
36174
36226
  );
36175
- if (segmentIndex === -1) {
36176
- return data;
36177
- }
36178
- const segment = data.corrected_segments[segmentIndex];
36179
- const wordIndex = segment.words.findIndex((word) => word.id === wordId);
36180
- if (wordIndex === -1) {
36181
- return data;
36182
- }
36183
- const updatedWords = segment.words.filter((_, index) => index !== wordIndex);
36184
- if (updatedWords.length > 0) {
36185
- const updatedSegment = {
36186
- ...segment,
36187
- words: updatedWords,
36188
- text: updatedWords.map((w) => w.text).join(" ")
36189
- };
36190
- return updateSegment(data, segmentIndex, updatedSegment);
36191
- } else {
36192
- return deleteSegment(data, segmentIndex);
36193
- }
36194
36227
  }
36195
36228
  const generateStorageKey = (data) => {
36196
36229
  var _a;
@@ -36519,7 +36552,11 @@ function Header({
36519
36552
  isUpdatingHandlers,
36520
36553
  onHandlerClick,
36521
36554
  onFindReplace,
36522
- onEditAll
36555
+ onEditAll,
36556
+ onUndo,
36557
+ onRedo,
36558
+ canUndo,
36559
+ canRedo
36523
36560
  }) {
36524
36561
  var _a, _b, _c;
36525
36562
  const theme2 = useTheme();
@@ -36684,6 +36721,40 @@ function Header({
36684
36721
  onChange: onModeChange
36685
36722
  }
36686
36723
  ),
36724
+ !isReadOnly && /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: { display: "flex", height: "32px" }, children: [
36725
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Tooltip, { title: "Undo (Cmd/Ctrl+Z)", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: /* @__PURE__ */ jsxRuntimeExports.jsx(
36726
+ IconButton,
36727
+ {
36728
+ size: "small",
36729
+ onClick: onUndo,
36730
+ disabled: !canUndo,
36731
+ sx: {
36732
+ border: `1px solid ${theme2.palette.divider}`,
36733
+ borderRadius: "4px",
36734
+ mx: 0.25,
36735
+ height: "32px",
36736
+ width: "32px"
36737
+ },
36738
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(UndoIcon, { fontSize: "small" })
36739
+ }
36740
+ ) }) }),
36741
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Tooltip, { title: "Redo (Cmd/Ctrl+Shift+Z)", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: /* @__PURE__ */ jsxRuntimeExports.jsx(
36742
+ IconButton,
36743
+ {
36744
+ size: "small",
36745
+ onClick: onRedo,
36746
+ disabled: !canRedo,
36747
+ sx: {
36748
+ border: `1px solid ${theme2.palette.divider}`,
36749
+ borderRadius: "4px",
36750
+ mx: 0.25,
36751
+ height: "32px",
36752
+ width: "32px"
36753
+ },
36754
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(RedoIcon, { fontSize: "small" })
36755
+ }
36756
+ ) }) })
36757
+ ] }),
36687
36758
  !isReadOnly && /* @__PURE__ */ jsxRuntimeExports.jsx(
36688
36759
  Button,
36689
36760
  {
@@ -37158,7 +37229,8 @@ const MemoizedTranscriptionView = reactExports.memo(function MemoizedTranscripti
37158
37229
  onPlaySegment,
37159
37230
  currentTime,
37160
37231
  anchors,
37161
- disableHighlighting
37232
+ disableHighlighting,
37233
+ onDataChange
37162
37234
  }) {
37163
37235
  return /* @__PURE__ */ jsxRuntimeExports.jsx(
37164
37236
  TranscriptionView,
@@ -37172,7 +37244,8 @@ const MemoizedTranscriptionView = reactExports.memo(function MemoizedTranscripti
37172
37244
  highlightInfo,
37173
37245
  onPlaySegment,
37174
37246
  currentTime: disableHighlighting ? void 0 : currentTime,
37175
- anchors
37247
+ anchors,
37248
+ onDataChange
37176
37249
  }
37177
37250
  );
37178
37251
  });
@@ -37224,7 +37297,11 @@ const MemoizedHeader = reactExports.memo(function MemoizedHeader2({
37224
37297
  isUpdatingHandlers,
37225
37298
  onHandlerClick,
37226
37299
  onFindReplace,
37227
- onEditAll
37300
+ onEditAll,
37301
+ onUndo,
37302
+ onRedo,
37303
+ canUndo,
37304
+ canRedo
37228
37305
  }) {
37229
37306
  return /* @__PURE__ */ jsxRuntimeExports.jsx(
37230
37307
  Header,
@@ -37242,7 +37319,11 @@ const MemoizedHeader = reactExports.memo(function MemoizedHeader2({
37242
37319
  isUpdatingHandlers,
37243
37320
  onHandlerClick,
37244
37321
  onFindReplace,
37245
- onEditAll
37322
+ onEditAll,
37323
+ onUndo,
37324
+ onRedo,
37325
+ canUndo,
37326
+ canRedo
37246
37327
  }
37247
37328
  );
37248
37329
  });
@@ -37258,7 +37339,6 @@ function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly,
37258
37339
  return availableSources.length > 0 ? availableSources[0] : "";
37259
37340
  });
37260
37341
  const [isReviewComplete, setIsReviewComplete] = reactExports.useState(false);
37261
- const [data, setData] = reactExports.useState(initialData);
37262
37342
  const [originalData] = reactExports.useState(() => JSON.parse(JSON.stringify(initialData)));
37263
37343
  const [interactionMode, setInteractionMode] = reactExports.useState("edit");
37264
37344
  const [isShiftPressed, setIsShiftPressed] = reactExports.useState(false);
@@ -37279,12 +37359,27 @@ function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly,
37279
37359
  const [isFindReplaceModalOpen, setIsFindReplaceModalOpen] = reactExports.useState(false);
37280
37360
  const theme2 = useTheme();
37281
37361
  const isMobile = useMediaQuery(theme2.breakpoints.down("md"));
37362
+ const [history, setHistory] = reactExports.useState([initialData]);
37363
+ const [historyIndex, setHistoryIndex] = reactExports.useState(0);
37364
+ const data = history[historyIndex];
37365
+ const updateDataWithHistory = reactExports.useCallback((newData, actionDescription) => {
37366
+ const newHistory = history.slice(0, historyIndex + 1);
37367
+ const deepCopiedNewData = JSON.parse(JSON.stringify(newData));
37368
+ newHistory.push(deepCopiedNewData);
37369
+ setHistory(newHistory);
37370
+ setHistoryIndex(newHistory.length - 1);
37371
+ }, [history, historyIndex]);
37372
+ reactExports.useEffect(() => {
37373
+ setHistory([initialData]);
37374
+ setHistoryIndex(0);
37375
+ }, [initialData]);
37282
37376
  reactExports.useEffect(() => {
37283
37377
  }, [initialData]);
37284
37378
  reactExports.useEffect(() => {
37285
37379
  const savedData = loadSavedData(initialData);
37286
37380
  if (savedData && window.confirm("Found saved progress for this song. Would you like to restore it?")) {
37287
- setData(savedData);
37381
+ setHistory([savedData]);
37382
+ setHistoryIndex(0);
37288
37383
  }
37289
37384
  }, [initialData]);
37290
37385
  reactExports.useEffect(() => {
@@ -37337,7 +37432,7 @@ function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly,
37337
37432
  var _a, _b, _c, _d, _e, _f, _g;
37338
37433
  if (effectiveMode === "delete_word") {
37339
37434
  const newData = deleteWord(data, info.word_id);
37340
- setData(newData);
37435
+ updateDataWithHistory(newData, "delete word");
37341
37436
  handleFlash("word");
37342
37437
  return;
37343
37438
  }
@@ -37448,17 +37543,24 @@ function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly,
37448
37543
  });
37449
37544
  }
37450
37545
  }
37451
- }, [data, effectiveMode, setModalContent, handleFlash, deleteWord]);
37546
+ }, [data, effectiveMode, setModalContent, handleFlash, deleteWord, updateDataWithHistory]);
37452
37547
  const handleUpdateSegment = reactExports.useCallback((updatedSegment) => {
37453
37548
  if (!editModalSegment) return;
37454
- const newData = updateSegment(data, editModalSegment.index, updatedSegment);
37455
- setData(newData);
37549
+ const currentData = history[historyIndex];
37550
+ const newSegments = currentData.corrected_segments.map(
37551
+ (segment, i) => i === editModalSegment.index ? updatedSegment : segment
37552
+ );
37553
+ const newDataImmutable = {
37554
+ ...currentData,
37555
+ corrected_segments: newSegments
37556
+ };
37557
+ updateDataWithHistory(newDataImmutable, "update segment");
37456
37558
  setEditModalSegment(null);
37457
- }, [data, editModalSegment]);
37559
+ }, [history, historyIndex, editModalSegment, updateDataWithHistory]);
37458
37560
  const handleDeleteSegment = reactExports.useCallback((segmentIndex) => {
37459
37561
  const newData = deleteSegment(data, segmentIndex);
37460
- setData(newData);
37461
- }, [data]);
37562
+ updateDataWithHistory(newData, "delete segment");
37563
+ }, [data, updateDataWithHistory]);
37462
37564
  const handleFinishReview = reactExports.useCallback(() => {
37463
37565
  setIsReviewModalOpen(true);
37464
37566
  }, []);
@@ -37483,7 +37585,8 @@ function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly,
37483
37585
  const handleResetCorrections = reactExports.useCallback(() => {
37484
37586
  if (window.confirm("Are you sure you want to reset all corrections? This cannot be undone.")) {
37485
37587
  clearSavedData(initialData);
37486
- setData(JSON.parse(JSON.stringify(initialData)));
37588
+ setHistory([JSON.parse(JSON.stringify(initialData))]);
37589
+ setHistoryIndex(0);
37487
37590
  setModalContent(null);
37488
37591
  setFlashingType(null);
37489
37592
  setHighlightInfo(null);
@@ -37492,20 +37595,20 @@ function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly,
37492
37595
  }, [initialData]);
37493
37596
  const handleAddSegment = reactExports.useCallback((beforeIndex) => {
37494
37597
  const newData = addSegmentBefore(data, beforeIndex);
37495
- setData(newData);
37496
- }, [data]);
37598
+ updateDataWithHistory(newData, "add segment");
37599
+ }, [data, updateDataWithHistory]);
37497
37600
  const handleSplitSegment = reactExports.useCallback((segmentIndex, afterWordIndex) => {
37498
37601
  const newData = splitSegment(data, segmentIndex, afterWordIndex);
37499
37602
  if (newData) {
37500
- setData(newData);
37603
+ updateDataWithHistory(newData, "split segment");
37501
37604
  setEditModalSegment(null);
37502
37605
  }
37503
- }, [data]);
37606
+ }, [data, updateDataWithHistory]);
37504
37607
  const handleMergeSegment = reactExports.useCallback((segmentIndex, mergeWithNext) => {
37505
37608
  const newData = mergeSegment(data, segmentIndex, mergeWithNext);
37506
- setData(newData);
37609
+ updateDataWithHistory(newData, "merge segment");
37507
37610
  setEditModalSegment(null);
37508
- }, [data]);
37611
+ }, [data, updateDataWithHistory]);
37509
37612
  const handleHandlerToggle = reactExports.useCallback(async (handler, enabled) => {
37510
37613
  if (!apiClient) return;
37511
37614
  try {
@@ -37517,7 +37620,7 @@ function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly,
37517
37620
  currentEnabled.delete(handler);
37518
37621
  }
37519
37622
  const newData = await apiClient.updateHandlers(Array.from(currentEnabled));
37520
- setData(newData);
37623
+ updateDataWithHistory(newData, `toggle handler ${handler}`);
37521
37624
  setModalContent(null);
37522
37625
  setFlashingType(null);
37523
37626
  setHighlightInfo(null);
@@ -37528,7 +37631,7 @@ function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly,
37528
37631
  } finally {
37529
37632
  setIsUpdatingHandlers(false);
37530
37633
  }
37531
- }, [apiClient, data.metadata.enabled_handlers, handleFlash]);
37634
+ }, [apiClient, data.metadata.enabled_handlers, handleFlash, updateDataWithHistory]);
37532
37635
  const handleHandlerClick = reactExports.useCallback((handler) => {
37533
37636
  setFlashingHandler(handler);
37534
37637
  setFlashingType("handler");
@@ -37545,14 +37648,14 @@ function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly,
37545
37648
  try {
37546
37649
  setIsAddingLyrics(true);
37547
37650
  const newData = await apiClient.addLyrics(source, lyrics);
37548
- setData(newData);
37651
+ updateDataWithHistory(newData, "add lyrics");
37549
37652
  } finally {
37550
37653
  setIsAddingLyrics(false);
37551
37654
  }
37552
- }, [apiClient]);
37655
+ }, [apiClient, updateDataWithHistory]);
37553
37656
  const handleFindReplace = (findText, replaceText, options) => {
37554
37657
  const newData = findAndReplace(data, findText, replaceText, options);
37555
- setData(newData);
37658
+ updateDataWithHistory(newData, "find/replace");
37556
37659
  };
37557
37660
  const handleEditAll = reactExports.useCallback(() => {
37558
37661
  console.log("EditAll - Starting process");
@@ -37685,19 +37788,70 @@ function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly,
37685
37788
  totalWordCount: updatedSegments.reduce((count, segment) => count + segment.words.length, 0),
37686
37789
  originalTotalWordCount: data.corrected_segments.reduce((count, segment) => count + segment.words.length, 0)
37687
37790
  });
37688
- setData({
37791
+ const newData = {
37689
37792
  ...data,
37690
37793
  corrected_segments: updatedSegments
37691
- });
37794
+ };
37795
+ updateDataWithHistory(newData, "edit all");
37692
37796
  setIsEditAllModalOpen(false);
37693
37797
  setGlobalEditSegment(null);
37694
- }, [data]);
37798
+ }, [data, updateDataWithHistory]);
37799
+ const handleUndo = reactExports.useCallback(() => {
37800
+ if (historyIndex > 0) {
37801
+ const newIndex = historyIndex - 1;
37802
+ setHistoryIndex(newIndex);
37803
+ }
37804
+ }, [historyIndex, history]);
37805
+ const handleRedo = reactExports.useCallback(() => {
37806
+ if (historyIndex < history.length - 1) {
37807
+ const newIndex = historyIndex + 1;
37808
+ setHistoryIndex(newIndex);
37809
+ }
37810
+ }, [historyIndex, history]);
37811
+ const canUndo = historyIndex > 0;
37812
+ const canRedo = historyIndex < history.length - 1;
37695
37813
  const metricClickHandlers = reactExports.useMemo(() => ({
37696
37814
  anchor: () => handleFlash("anchor"),
37697
37815
  corrected: () => handleFlash("corrected"),
37698
37816
  uncorrected: () => handleFlash("uncorrected")
37699
37817
  }), [handleFlash]);
37700
37818
  const isAnyModalOpenMemo = reactExports.useMemo(() => isAnyModalOpen, [isAnyModalOpen]);
37819
+ reactExports.useEffect(() => {
37820
+ const { handleKeyDown: baseHandleKeyDown, handleKeyUp, cleanup } = setupKeyboardHandlers({
37821
+ setIsShiftPressed,
37822
+ setIsCtrlPressed
37823
+ });
37824
+ const handleKeyDown = (e) => {
37825
+ const targetElement = e.target;
37826
+ const isInputFocused = targetElement.tagName === "INPUT" || targetElement.tagName === "TEXTAREA";
37827
+ if (!isAnyModalOpen && !isInputFocused) {
37828
+ const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
37829
+ const modifierKey = isMac ? e.metaKey : e.ctrlKey;
37830
+ if (modifierKey && e.key.toLowerCase() === "z") {
37831
+ e.preventDefault();
37832
+ if (e.shiftKey) {
37833
+ if (canRedo) handleRedo();
37834
+ } else {
37835
+ if (canUndo) handleUndo();
37836
+ }
37837
+ return;
37838
+ }
37839
+ }
37840
+ baseHandleKeyDown(e);
37841
+ };
37842
+ window.addEventListener("keydown", handleKeyDown);
37843
+ window.addEventListener("keyup", handleKeyUp);
37844
+ if (isAnyModalOpen) {
37845
+ setIsShiftPressed(false);
37846
+ setIsCtrlPressed(false);
37847
+ }
37848
+ return () => {
37849
+ window.removeEventListener("keydown", handleKeyDown);
37850
+ window.removeEventListener("keyup", handleKeyUp);
37851
+ document.body.style.userSelect = "";
37852
+ cleanup();
37853
+ };
37854
+ }, [setIsShiftPressed, setIsCtrlPressed, isAnyModalOpen, handleUndo, handleRedo, canUndo, canRedo]);
37701
37855
  return /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: {
37702
37856
  p: 1,
37703
37857
  pb: 3,
@@ -37721,7 +37875,11 @@ function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly,
37721
37875
  onHandlerClick: handleHandlerClick,
37722
37876
  onAddLyrics: () => setIsAddLyricsModalOpen(true),
37723
37877
  onFindReplace: () => setIsFindReplaceModalOpen(true),
37724
- onEditAll: handleEditAll
37878
+ onEditAll: handleEditAll,
37879
+ onUndo: handleUndo,
37880
+ onRedo: handleRedo,
37881
+ canUndo,
37882
+ canRedo
37725
37883
  }
37726
37884
  ),
37727
37885
  /* @__PURE__ */ jsxRuntimeExports.jsxs(Grid, { container: true, direction: isMobile ? "column" : "row", children: [
@@ -37739,7 +37897,10 @@ function LyricsAnalyzer({ data: initialData, onFileLoad, apiClient, isReadOnly,
37739
37897
  onPlaySegment: handlePlaySegment,
37740
37898
  currentTime: currentAudioTime,
37741
37899
  anchors: data.anchor_sequences,
37742
- disableHighlighting: isAnyModalOpenMemo
37900
+ disableHighlighting: isAnyModalOpenMemo,
37901
+ onDataChange: (updatedData) => {
37902
+ updateDataWithHistory(updatedData, "direct data change");
37903
+ }
37743
37904
  }
37744
37905
  ),
37745
37906
  !isReadOnly && apiClient && /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { sx: {
@@ -38221,4 +38382,4 @@ ReactDOM$1.createRoot(document.getElementById("root")).render(
38221
38382
  /* @__PURE__ */ jsxRuntimeExports.jsx(App, {})
38222
38383
  ] })
38223
38384
  );
38224
- //# sourceMappingURL=index-2vK-qVJS.js.map
38385
+ //# sourceMappingURL=index-BpvPgWoc.js.map