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 ? (
-
+
) : (