clean
This commit is contained in:
@@ -411,7 +411,9 @@ exports.getSunoTimestamps = onCall(async ({ data = {} }) => {
|
|||||||
.doc(projectId)
|
.doc(projectId)
|
||||||
.set(
|
.set(
|
||||||
{
|
{
|
||||||
[`musicTimestamps.${index}`]: dataToReturn,
|
musicTimestamps: {
|
||||||
|
[musicIndex]: dataToReturn,
|
||||||
|
},
|
||||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
},
|
},
|
||||||
{ merge: true }
|
{ merge: true }
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import React, { useMemo } from "react";
|
||||||
|
import { Text, View } from "react-native";
|
||||||
|
import { Palette } from "../styles";
|
||||||
|
import { FONT_FAMILY } from "../styles/Fonts";
|
||||||
|
|
||||||
|
export function groupAlignedWordsToLines(alignedWords = [], { removeTags = true } = {}) {
|
||||||
|
const out = [];
|
||||||
|
let buf = [];
|
||||||
|
let start = null;
|
||||||
|
const clean = (txt) =>
|
||||||
|
String(txt || "")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
const isSentenceEnd = (txt) => /[.!?]$/.test((txt || "").trim());
|
||||||
|
const isSectionTag = (txt) => /^\s*\[[^\]]+\]\s*$/i.test((txt || "").trim());
|
||||||
|
|
||||||
|
for (let i = 0; i < alignedWords.length; i++) {
|
||||||
|
const w = alignedWords[i] || {};
|
||||||
|
const original = String(w.word || "");
|
||||||
|
const textNoNewline = original.replace(/\n/g, " ");
|
||||||
|
const textNoTag = removeTags
|
||||||
|
? textNoNewline.replace(/^\s*\[[^\]]+\]\s*/i, "")
|
||||||
|
: textNoNewline;
|
||||||
|
const next = alignedWords[i + 1] || null;
|
||||||
|
const gapToNext = next
|
||||||
|
? Math.max(0, Number(next.startS || 0) - Number(w.endS || 0))
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
if (buf.length === 0) start = Number(w.startS || 0);
|
||||||
|
|
||||||
|
if (isSectionTag(textNoNewline)) {
|
||||||
|
if (!removeTags) {
|
||||||
|
const joined = clean(textNoNewline);
|
||||||
|
if (joined)
|
||||||
|
out.push({
|
||||||
|
text: joined,
|
||||||
|
startS: start ?? Number(w.startS || 0),
|
||||||
|
endS: Number(w.endS || 0),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
buf = [];
|
||||||
|
start = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!clean(textNoTag)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.push(textNoTag);
|
||||||
|
|
||||||
|
const eolByNewline = /\n/.test(original);
|
||||||
|
const eolByPause = gapToNext >= 0.6; // threshold for a logical break
|
||||||
|
const eolByPunct = isSentenceEnd(textNoTag);
|
||||||
|
const isLast = i === alignedWords.length - 1;
|
||||||
|
|
||||||
|
if (eolByNewline || eolByPause || eolByPunct || isLast) {
|
||||||
|
const joined = clean(buf.join(" "));
|
||||||
|
if (joined)
|
||||||
|
out.push({
|
||||||
|
text: joined,
|
||||||
|
startS: start ?? Number(w.startS || 0),
|
||||||
|
endS: Number(w.endS || 0),
|
||||||
|
});
|
||||||
|
buf = [];
|
||||||
|
start = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function KaraokeLyrics({
|
||||||
|
alignedWords = [],
|
||||||
|
currentTimeS = 0,
|
||||||
|
showContext = true,
|
||||||
|
removeTags = true,
|
||||||
|
}) {
|
||||||
|
const lines = useMemo(
|
||||||
|
() => groupAlignedWordsToLines(alignedWords, { removeTags }),
|
||||||
|
[alignedWords, removeTags]
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentLineIdx = useMemo(() => {
|
||||||
|
if (!lines || lines.length === 0) return -1;
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const L = lines[i];
|
||||||
|
if (currentTimeS >= (L.startS || 0) && currentTimeS <= (L.endS || 0))
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
if (currentTimeS > (lines[lines.length - 1]?.endS || 0))
|
||||||
|
return lines.length - 1;
|
||||||
|
return -1;
|
||||||
|
}, [lines, currentTimeS]);
|
||||||
|
|
||||||
|
if (!lines.length) return null;
|
||||||
|
|
||||||
|
const prev =
|
||||||
|
showContext && currentLineIdx > 0 ? lines[currentLineIdx - 1]?.text : "";
|
||||||
|
const curr =
|
||||||
|
currentLineIdx >= 0 ? lines[currentLineIdx]?.text : lines[0]?.text;
|
||||||
|
const next =
|
||||||
|
showContext && currentLineIdx + 1 < lines.length
|
||||||
|
? lines[currentLineIdx + 1]?.text
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{ gap: 4 }}>
|
||||||
|
{/* {prev ? (
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: "#FFFFFF99",
|
||||||
|
fontSize: 14,
|
||||||
|
textAlign: "center",
|
||||||
|
fontFamily: FONT_FAMILY.InterMedium,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{prev}
|
||||||
|
</Text>
|
||||||
|
) : null} */}
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: Palette.white,
|
||||||
|
fontSize: 18,
|
||||||
|
textAlign: "center",
|
||||||
|
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{curr}
|
||||||
|
</Text>
|
||||||
|
{next ? (
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: "#FFFFFF99",
|
||||||
|
fontSize: 14,
|
||||||
|
textAlign: "center",
|
||||||
|
fontFamily: FONT_FAMILY.InterMedium,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{next}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ const Playback = ({ route }) => {
|
|||||||
return;
|
return;
|
||||||
// return navigate(Routes.RecordPlayback, { project });
|
// return navigate(Routes.RecordPlayback, { project });
|
||||||
}
|
}
|
||||||
if (project?.[`musicTimestamps.${songIndex}`]) {
|
if (project?.musicTimestamps?.[songIndex]) {
|
||||||
console.log("exists");
|
console.log("exists");
|
||||||
return navigate(Routes.RecordPlayback, { project });
|
return navigate(Routes.RecordPlayback, { project });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { Text, View } from "react-native";
|
|||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import Svg, { Circle } from "react-native-svg";
|
import Svg, { Circle } from "react-native-svg";
|
||||||
import GradientButton from "../../components/GradientButton";
|
import GradientButton from "../../components/GradientButton";
|
||||||
|
import KaraokeLyrics from "../../components/KaraokeLyrics";
|
||||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||||
import { increment, projectsRef } from "../../config/firebase";
|
import { increment, projectsRef } from "../../config/firebase";
|
||||||
import { Routes } from "../../navigation";
|
import { Routes } from "../../navigation";
|
||||||
@@ -60,15 +61,67 @@ const RecordPlayback = ({ route }) => {
|
|||||||
}, [songIndex]);
|
}, [songIndex]);
|
||||||
|
|
||||||
const alignedWords = useMemo(() => {
|
const alignedWords = useMemo(() => {
|
||||||
const ts = project?.[`musicTimestamps.${songIndex}`];
|
const ts = project?.musicTimestamps?.[musicIndex];
|
||||||
null;
|
|
||||||
const arr = Array.isArray(ts?.alignedWords) ? ts.alignedWords : [];
|
const arr = Array.isArray(ts?.alignedWords) ? ts.alignedWords : [];
|
||||||
return arr.map((w) => ({
|
return arr.map((w) => ({
|
||||||
word: String(w?.word ?? "").replace(/\n/g, " "),
|
word: String(w?.word ?? ""),
|
||||||
startS: Number(w?.startS ?? 0),
|
startS: Number(w?.startS ?? 0),
|
||||||
endS: Number(w?.endS ?? 0),
|
endS: Number(w?.endS ?? 0),
|
||||||
}));
|
}));
|
||||||
}, [project?.musicTimestamps, musicIndex]);
|
}, [project?.musicTimestamps, musicIndex]);
|
||||||
|
|
||||||
|
// Build line groups to display line-by-line
|
||||||
|
const lines = useMemo(() => {
|
||||||
|
const out = [];
|
||||||
|
let buf = [];
|
||||||
|
let start = null;
|
||||||
|
const clean = (txt) =>
|
||||||
|
String(txt || "")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
const isSentenceEnd = (txt) => /[.!?]$/.test((txt || "").trim());
|
||||||
|
const isSectionTag = (txt) =>
|
||||||
|
/^\s*\[[^\]]+\]\s*$/i.test((txt || "").trim());
|
||||||
|
for (let i = 0; i < alignedWords.length; i++) {
|
||||||
|
const w = alignedWords[i];
|
||||||
|
const original = String(w.word || "");
|
||||||
|
const textNoNewline = original.replace(/\n/g, " ");
|
||||||
|
const gapToNext =
|
||||||
|
i < alignedWords.length - 1
|
||||||
|
? Math.max(0, alignedWords[i + 1].startS - w.endS)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// If buffer empty, mark start
|
||||||
|
if (buf.length === 0) start = w.startS;
|
||||||
|
|
||||||
|
// Section tag becomes its own line
|
||||||
|
if (isSectionTag(textNoNewline)) {
|
||||||
|
const joined = clean(textNoNewline);
|
||||||
|
if (joined)
|
||||||
|
out.push({ text: joined, startS: start ?? w.startS, endS: w.endS });
|
||||||
|
buf = [];
|
||||||
|
start = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.push(textNoNewline);
|
||||||
|
|
||||||
|
const eolByNewline = /\n/.test(original);
|
||||||
|
const eolByPause = gapToNext >= 0.6;
|
||||||
|
const eolByPunct = isSentenceEnd(textNoNewline);
|
||||||
|
const isLast = i === alignedWords.length - 1;
|
||||||
|
|
||||||
|
if (eolByNewline || eolByPause || eolByPunct || isLast) {
|
||||||
|
const joined = clean(buf.join(" "));
|
||||||
|
if (joined)
|
||||||
|
out.push({ text: joined, startS: start ?? w.startS, endS: w.endS });
|
||||||
|
buf = [];
|
||||||
|
start = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}, [alignedWords]);
|
||||||
// console.log("alignedWords:", alignedWords);
|
// console.log("alignedWords:", alignedWords);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
listenedMsRef.current = 0;
|
listenedMsRef.current = 0;
|
||||||
@@ -88,18 +141,18 @@ const RecordPlayback = ({ route }) => {
|
|||||||
return () => clearInterval(id);
|
return () => clearInterval(id);
|
||||||
}, [player]);
|
}, [player]);
|
||||||
|
|
||||||
// Compute current word index from playback time
|
// Compute current line index from playback time
|
||||||
const currentTimeS = (progressInfo?.pos || 0) / 1000;
|
const currentTimeS = (progressInfo?.pos || 0) / 1000;
|
||||||
const currentIdx = useMemo(() => {
|
const currentLineIdx = useMemo(() => {
|
||||||
if (!alignedWords || alignedWords.length === 0) return -1;
|
if (!lines || lines.length === 0) return -1;
|
||||||
for (let i = 0; i < alignedWords.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
const w = alignedWords[i];
|
const L = lines[i];
|
||||||
if (currentTimeS >= w.startS && currentTimeS <= w.endS) return i;
|
if (currentTimeS >= L.startS && currentTimeS <= L.endS) return i;
|
||||||
}
|
}
|
||||||
if (currentTimeS > (alignedWords[alignedWords.length - 1]?.endS || 0))
|
if (currentTimeS > (lines[lines.length - 1]?.endS || 0))
|
||||||
return alignedWords.length - 1;
|
return lines.length - 1;
|
||||||
return -1;
|
return -1;
|
||||||
}, [alignedWords, currentTimeS]);
|
}, [lines, currentTimeS]);
|
||||||
|
|
||||||
// Permissions au mount + cleanup
|
// Permissions au mount + cleanup
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -442,7 +495,10 @@ const RecordPlayback = ({ route }) => {
|
|||||||
>
|
>
|
||||||
<CreateLyricsHeader>
|
<CreateLyricsHeader>
|
||||||
{(isRecording || showProgress) && alignedWords?.length > 0 ? (
|
{(isRecording || showProgress) && alignedWords?.length > 0 ? (
|
||||||
<KaraokeLine words={alignedWords} currentIdx={currentIdx} />
|
<KaraokeLyrics
|
||||||
|
alignedWords={alignedWords}
|
||||||
|
currentTimeS={(progressInfo?.pos || 0) / 1000}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
@@ -494,42 +550,41 @@ const ProgressRing = ({ size = 50, strokeWidth = 12, progress = 0 }) => {
|
|||||||
|
|
||||||
export default RecordPlayback;
|
export default RecordPlayback;
|
||||||
|
|
||||||
// Simple karaoke line renderer
|
// Render previous/current/next line to reduce jumpiness
|
||||||
const KaraokeLine = ({ words = [], currentIdx = -1 }) => {
|
const KaraokeLines = ({ lines = [], currentLineIdx = -1 }) => {
|
||||||
const windowSize = 8;
|
const prev = currentLineIdx > 0 ? lines[currentLineIdx - 1]?.text : "";
|
||||||
const start = Math.max(0, currentIdx - 2);
|
const curr = currentLineIdx >= 0 ? lines[currentLineIdx]?.text : "";
|
||||||
const end = Math.min(
|
const next =
|
||||||
words.length,
|
currentLineIdx + 1 < lines.length ? lines[currentLineIdx + 1]?.text : "";
|
||||||
currentIdx >= 0 ? currentIdx + windowSize : windowSize
|
|
||||||
);
|
|
||||||
const slice = words.slice(start, end);
|
|
||||||
return (
|
return (
|
||||||
<Text
|
<View style={{ gap: 4 }}>
|
||||||
style={{
|
{/* {prev ? (
|
||||||
color: Palette.white,
|
<Text style={{ color: "#FFFFFF99", fontSize: 14, textAlign: "center", fontFamily: FONT_FAMILY.InterMedium }}>
|
||||||
fontSize: 18,
|
{prev}
|
||||||
lineHeight: 24,
|
</Text>
|
||||||
textAlign: "center",
|
) : null} */}
|
||||||
}}
|
<Text
|
||||||
>
|
style={{
|
||||||
{slice.map((w, i) => {
|
color: Palette.white,
|
||||||
const idx = start + i;
|
fontSize: 18,
|
||||||
const text = String(w.word || "").replace(/\s+/g, " ");
|
textAlign: "center",
|
||||||
const isCurrent = idx === currentIdx;
|
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||||
return (
|
}}
|
||||||
<Text
|
>
|
||||||
key={`${idx}-${text}`}
|
{curr}
|
||||||
style={{
|
</Text>
|
||||||
color: isCurrent ? Palette.white : "#FFFFFFAA",
|
{next ? (
|
||||||
fontFamily: isCurrent
|
<Text
|
||||||
? FONT_FAMILY.InterSemiBold
|
style={{
|
||||||
: FONT_FAMILY.InterMedium,
|
color: "#FFFFFF99",
|
||||||
}}
|
fontSize: 14,
|
||||||
>
|
textAlign: "center",
|
||||||
{text + " "}
|
fontFamily: FONT_FAMILY.InterMedium,
|
||||||
</Text>
|
}}
|
||||||
);
|
>
|
||||||
})}
|
{next}
|
||||||
</Text>
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { Image, Platform, Pressable, Text, View } from "react-native";
|
|||||||
import Carousel from "react-native-reanimated-carousel";
|
import Carousel from "react-native-reanimated-carousel";
|
||||||
import { responsiveHeight } from "react-native-responsive-dimensions";
|
import { responsiveHeight } from "react-native-responsive-dimensions";
|
||||||
import { icons, img } from "../assets";
|
import { icons, img } from "../assets";
|
||||||
|
import KaraokeLyrics from "../components/KaraokeLyrics";
|
||||||
import {
|
import {
|
||||||
arrayRemove,
|
arrayRemove,
|
||||||
arrayUnion,
|
arrayUnion,
|
||||||
@@ -163,6 +164,34 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
|
|||||||
};
|
};
|
||||||
}, [audioPlayer, videoPlayer]);
|
}, [audioPlayer, videoPlayer]);
|
||||||
|
|
||||||
|
// Build alignedWords from item musicTimestamps
|
||||||
|
const alignedWords = useMemo(() => {
|
||||||
|
const idx = Number(item?.songIndex) || 0;
|
||||||
|
const ts = item?.musicTimestamps?.[idx];
|
||||||
|
|
||||||
|
const arr = Array.isArray(ts?.alignedWords) ? ts.alignedWords : [];
|
||||||
|
return arr.map((w) => ({
|
||||||
|
word: String(w?.word ?? ""),
|
||||||
|
startS: Number(w?.startS ?? 0),
|
||||||
|
endS: Number(w?.endS ?? 0),
|
||||||
|
}));
|
||||||
|
}, [item?.musicTimestamps, item?.songIndex]);
|
||||||
|
|
||||||
|
// Track current time for lyrics sync
|
||||||
|
const [currentTimeS, setCurrentTimeS] = useState(0);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isActive) return;
|
||||||
|
const id = setInterval(() => {
|
||||||
|
try {
|
||||||
|
const t = hasExternalAudio
|
||||||
|
? Number(audioPlayer?.currentTime || 0)
|
||||||
|
: Number(videoPlayer?.currentTime || 0);
|
||||||
|
setCurrentTimeS(t);
|
||||||
|
} catch (e) {}
|
||||||
|
}, 250);
|
||||||
|
return () => clearInterval(id);
|
||||||
|
}, [isActive, hasExternalAudio, audioPlayer, videoPlayer]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
@@ -343,15 +372,22 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
|
|||||||
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
|
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Text
|
{alignedWords?.length > 0 ? (
|
||||||
style={{
|
<KaraokeLyrics
|
||||||
fontSize: 12,
|
alignedWords={alignedWords}
|
||||||
color: Palette.white,
|
currentTimeS={currentTimeS}
|
||||||
fontFamily: FONT_FAMILY.InterRegular,
|
/>
|
||||||
}}
|
) : (
|
||||||
>
|
<Text
|
||||||
{item?.title || "Description chanson"}
|
style={{
|
||||||
</Text>
|
fontSize: 12,
|
||||||
|
color: Palette.white,
|
||||||
|
fontFamily: FONT_FAMILY.InterRegular,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item?.title || "Description chanson"}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</BlurView>
|
</BlurView>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
Reference in New Issue
Block a user