From 5bdf268ebf0413745e5efb2a3ef4f86bf7bf9c34 Mon Sep 17 00:00:00 2001 From: Leon Date: Tue, 24 Feb 2026 11:36:19 +0100 Subject: [PATCH] karaoke lines vertical --- src/components/KaraokeLyrics.js | 200 ++++++++++++++++++++- src/screens/Playback/RecordPlayback.js | 5 +- src/screens/Playback/RecordPlayback.web.js | 10 +- 3 files changed, 206 insertions(+), 9 deletions(-) diff --git a/src/components/KaraokeLyrics.js b/src/components/KaraokeLyrics.js index babdb2f..034fbd1 100644 --- a/src/components/KaraokeLyrics.js +++ b/src/components/KaraokeLyrics.js @@ -65,28 +65,216 @@ export function groupAlignedWordsToLines(alignedWords = [], { removeTags = true return out } +export function groupAlignedWordsToLineItems( + 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()) + const pushLine = (words, endS) => { + const text = clean(words.map((item) => item.text).join(' ')) + if (!text) return + out.push({ + text, + startS: start ?? Number(words[0]?.startS || 0), + endS: Number(endS || words[words.length - 1]?.endS || 0), + words, + }) + } + + 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 + 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), + words: [ + { + text: joined, + startS: Number(w.startS || 0), + endS: Number(w.endS || 0), + }, + ], + }) + } + } + buf = [] + start = null + continue + } + + if (!clean(textNoTag)) { + continue + } + + const wordItem = { + text: textNoTag, + startS: Number(w.startS || 0), + endS: Number(w.endS || 0), + } + + buf.push(wordItem) + + const eolByNewline = /\n/.test(original) + const eolByPunct = isSentenceEnd(textNoTag) + const isLast = i === alignedWords.length - 1 + + if (buf.length && (eolByNewline || eolByPunct || isLast)) { + pushLine(buf, Number(w.endS || 0)) + buf = [] + start = null + } + } + return out +} + +const clamp01 = (value) => Math.max(0, Math.min(1, value)) + +const alphaWhite = (alpha) => `rgba(255,255,255,${alpha})` + export default function KaraokeLyrics({ alignedWords = [], currentTimeS = 0, showContext = true, removeTags = true, + mode = 'focus', + teleprompterLines = 6, + teleprompterFontSize = 18, + teleprompterLineHeight = 28, + teleprompterAnchorLine = 1, }) { const lines = useMemo( () => groupAlignedWordsToLines(alignedWords, { removeTags }), [alignedWords, removeTags] ) + const teleprompterData = useMemo( + () => groupAlignedWordsToLineItems(alignedWords, { removeTags }), + [alignedWords, removeTags] + ) + + const activeLines = mode === 'teleprompter' ? teleprompterData : lines + const currentLineIdx = useMemo(() => { - if (!lines || lines.length === 0) return -1 - for (let i = 0; i < lines.length; i++) { - const L = lines[i] + if (!activeLines || activeLines.length === 0) return -1 + for (let i = 0; i < activeLines.length; i++) { + const L = activeLines[i] if (currentTimeS >= (L.startS || 0) && currentTimeS <= (L.endS || 0)) return i } - if (currentTimeS > (lines[lines.length - 1]?.endS || 0)) return lines.length - 1 + if (currentTimeS > (activeLines[activeLines.length - 1]?.endS || 0)) + return activeLines.length - 1 return -1 - }, [lines, currentTimeS]) + }, [activeLines, currentTimeS]) - if (!lines.length) return null + if (!activeLines.length) return null + + if (mode === 'teleprompter') { + const safeLines = Math.max(1, teleprompterLines) + const containerHeight = teleprompterLineHeight * safeLines + const anchorLine = Math.max(0, Math.min(safeLines - 1, teleprompterAnchorLine)) + const anchorOffset = teleprompterLineHeight * anchorLine + const safeIdx = Math.max(0, currentLineIdx) + const translateY = anchorOffset - safeIdx * teleprompterLineHeight + const inactiveAlpha = 0.35 + const completedAlpha = 0.75 + const activeAlpha = 1 + + return ( + + + {activeLines.map((line, idx) => { + const isUpcoming = currentTimeS < (line.startS || 0) + const isPast = currentTimeS > (line.endS || 0) + const lineKey = `${line.startS}-${idx}` + + if (isUpcoming) { + return ( + + {line.text} + + ) + } + + if (isPast) { + return ( + + {line.text} + + ) + } + + return ( + + {line.words.map((word, wordIndex) => { + const duration = Math.max(0.001, (word.endS || 0) - (word.startS || 0)) + const progress = clamp01((currentTimeS - (word.startS || 0)) / duration) + const alpha = inactiveAlpha + (activeAlpha - inactiveAlpha) * progress + const text = word.text + return ( + + {text} + {wordIndex < line.words.length - 1 ? ' ' : ''} + + ) + })} + + ) + })} + + + ) + } const prev = showContext && currentLineIdx > 0 ? lines[currentLineIdx - 1]?.text : '' const curr = currentLineIdx >= 0 ? lines[currentLineIdx]?.text : lines[0]?.text diff --git a/src/screens/Playback/RecordPlayback.js b/src/screens/Playback/RecordPlayback.js index 1097a4d..2fa696a 100644 --- a/src/screens/Playback/RecordPlayback.js +++ b/src/screens/Playback/RecordPlayback.js @@ -1090,7 +1090,7 @@ const RecordPlayback = ({ route }) => { position: 'absolute', left: 0, right: 0, - bottom: bottom + 200, + top: top + 90, paddingHorizontal: gutters, }} > @@ -1099,6 +1099,9 @@ const RecordPlayback = ({ route }) => { ) : ( { position: 'absolute', left: 0, right: 0, - bottom: bottom + 180, + top: top + 90, paddingHorizontal: gutters, }} > {isRecording && alignedWords.length > 0 ? ( - + ) : (