clean
This commit is contained in:
@@ -411,7 +411,9 @@ exports.getSunoTimestamps = onCall(async ({ data = {} }) => {
|
||||
.doc(projectId)
|
||||
.set(
|
||||
{
|
||||
[`musicTimestamps.${index}`]: dataToReturn,
|
||||
musicTimestamps: {
|
||||
[musicIndex]: dataToReturn,
|
||||
},
|
||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
},
|
||||
{ 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 navigate(Routes.RecordPlayback, { project });
|
||||
}
|
||||
if (project?.[`musicTimestamps.${songIndex}`]) {
|
||||
if (project?.musicTimestamps?.[songIndex]) {
|
||||
console.log("exists");
|
||||
return navigate(Routes.RecordPlayback, { project });
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Text, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import Svg, { Circle } from "react-native-svg";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
import KaraokeLyrics from "../../components/KaraokeLyrics";
|
||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||
import { increment, projectsRef } from "../../config/firebase";
|
||||
import { Routes } from "../../navigation";
|
||||
@@ -60,15 +61,67 @@ const RecordPlayback = ({ route }) => {
|
||||
}, [songIndex]);
|
||||
|
||||
const alignedWords = useMemo(() => {
|
||||
const ts = project?.[`musicTimestamps.${songIndex}`];
|
||||
null;
|
||||
const ts = project?.musicTimestamps?.[musicIndex];
|
||||
|
||||
const arr = Array.isArray(ts?.alignedWords) ? ts.alignedWords : [];
|
||||
return arr.map((w) => ({
|
||||
word: String(w?.word ?? "").replace(/\n/g, " "),
|
||||
word: String(w?.word ?? ""),
|
||||
startS: Number(w?.startS ?? 0),
|
||||
endS: Number(w?.endS ?? 0),
|
||||
}));
|
||||
}, [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);
|
||||
useEffect(() => {
|
||||
listenedMsRef.current = 0;
|
||||
@@ -88,18 +141,18 @@ const RecordPlayback = ({ route }) => {
|
||||
return () => clearInterval(id);
|
||||
}, [player]);
|
||||
|
||||
// Compute current word index from playback time
|
||||
// Compute current line index from playback time
|
||||
const currentTimeS = (progressInfo?.pos || 0) / 1000;
|
||||
const currentIdx = useMemo(() => {
|
||||
if (!alignedWords || alignedWords.length === 0) return -1;
|
||||
for (let i = 0; i < alignedWords.length; i++) {
|
||||
const w = alignedWords[i];
|
||||
if (currentTimeS >= w.startS && currentTimeS <= w.endS) return i;
|
||||
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 && currentTimeS <= L.endS) return i;
|
||||
}
|
||||
if (currentTimeS > (alignedWords[alignedWords.length - 1]?.endS || 0))
|
||||
return alignedWords.length - 1;
|
||||
if (currentTimeS > (lines[lines.length - 1]?.endS || 0))
|
||||
return lines.length - 1;
|
||||
return -1;
|
||||
}, [alignedWords, currentTimeS]);
|
||||
}, [lines, currentTimeS]);
|
||||
|
||||
// Permissions au mount + cleanup
|
||||
useEffect(() => {
|
||||
@@ -442,7 +495,10 @@ const RecordPlayback = ({ route }) => {
|
||||
>
|
||||
<CreateLyricsHeader>
|
||||
{(isRecording || showProgress) && alignedWords?.length > 0 ? (
|
||||
<KaraokeLine words={alignedWords} currentIdx={currentIdx} />
|
||||
<KaraokeLyrics
|
||||
alignedWords={alignedWords}
|
||||
currentTimeS={(progressInfo?.pos || 0) / 1000}
|
||||
/>
|
||||
) : (
|
||||
<Text
|
||||
style={{
|
||||
@@ -494,42 +550,41 @@ const ProgressRing = ({ size = 50, strokeWidth = 12, progress = 0 }) => {
|
||||
|
||||
export default RecordPlayback;
|
||||
|
||||
// Simple karaoke line renderer
|
||||
const KaraokeLine = ({ words = [], currentIdx = -1 }) => {
|
||||
const windowSize = 8;
|
||||
const start = Math.max(0, currentIdx - 2);
|
||||
const end = Math.min(
|
||||
words.length,
|
||||
currentIdx >= 0 ? currentIdx + windowSize : windowSize
|
||||
);
|
||||
const slice = words.slice(start, end);
|
||||
// Render previous/current/next line to reduce jumpiness
|
||||
const KaraokeLines = ({ lines = [], currentLineIdx = -1 }) => {
|
||||
const prev = currentLineIdx > 0 ? lines[currentLineIdx - 1]?.text : "";
|
||||
const curr = currentLineIdx >= 0 ? lines[currentLineIdx]?.text : "";
|
||||
const next =
|
||||
currentLineIdx + 1 < lines.length ? lines[currentLineIdx + 1]?.text : "";
|
||||
return (
|
||||
<Text
|
||||
style={{
|
||||
color: Palette.white,
|
||||
fontSize: 18,
|
||||
lineHeight: 24,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{slice.map((w, i) => {
|
||||
const idx = start + i;
|
||||
const text = String(w.word || "").replace(/\s+/g, " ");
|
||||
const isCurrent = idx === currentIdx;
|
||||
return (
|
||||
<Text
|
||||
key={`${idx}-${text}`}
|
||||
style={{
|
||||
color: isCurrent ? Palette.white : "#FFFFFFAA",
|
||||
fontFamily: isCurrent
|
||||
? FONT_FAMILY.InterSemiBold
|
||||
: FONT_FAMILY.InterMedium,
|
||||
}}
|
||||
>
|
||||
{text + " "}
|
||||
</Text>
|
||||
);
|
||||
})}
|
||||
</Text>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Image, Platform, Pressable, Text, View } from "react-native";
|
||||
import Carousel from "react-native-reanimated-carousel";
|
||||
import { responsiveHeight } from "react-native-responsive-dimensions";
|
||||
import { icons, img } from "../assets";
|
||||
import KaraokeLyrics from "../components/KaraokeLyrics";
|
||||
import {
|
||||
arrayRemove,
|
||||
arrayUnion,
|
||||
@@ -163,6 +164,34 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
|
||||
};
|
||||
}, [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 (
|
||||
<View
|
||||
style={{
|
||||
@@ -343,15 +372,22 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
|
||||
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
{item?.title || "Description chanson"}
|
||||
</Text>
|
||||
{alignedWords?.length > 0 ? (
|
||||
<KaraokeLyrics
|
||||
alignedWords={alignedWords}
|
||||
currentTimeS={currentTimeS}
|
||||
/>
|
||||
) : (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
{item?.title || "Description chanson"}
|
||||
</Text>
|
||||
)}
|
||||
</BlurView>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
Reference in New Issue
Block a user