karaoke lines vertical

This commit is contained in:
2026-02-24 11:36:19 +01:00
parent c25e15a914
commit 5bdf268ebf
3 changed files with 206 additions and 9 deletions
+194 -6
View File
@@ -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 (
<View
style={{
height: containerHeight,
overflow: 'hidden',
}}
>
<View
style={{
transform: [{ translateY }],
}}
>
{activeLines.map((line, idx) => {
const isUpcoming = currentTimeS < (line.startS || 0)
const isPast = currentTimeS > (line.endS || 0)
const lineKey = `${line.startS}-${idx}`
if (isUpcoming) {
return (
<Text
key={lineKey}
style={{
color: alphaWhite(inactiveAlpha),
fontSize: teleprompterFontSize,
lineHeight: teleprompterLineHeight,
textAlign: 'center',
fontFamily: FONT_FAMILY.InterSemiBold,
}}
>
{line.text}
</Text>
)
}
if (isPast) {
return (
<Text
key={lineKey}
style={{
color: alphaWhite(completedAlpha),
fontSize: teleprompterFontSize,
lineHeight: teleprompterLineHeight,
textAlign: 'center',
fontFamily: FONT_FAMILY.InterSemiBold,
}}
>
{line.text}
</Text>
)
}
return (
<Text
key={lineKey}
style={{
fontSize: teleprompterFontSize,
lineHeight: teleprompterLineHeight,
textAlign: 'center',
fontFamily: FONT_FAMILY.InterSemiBold,
}}
>
{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 key={`${lineKey}-${wordIndex}`} style={{ color: alphaWhite(alpha) }}>
{text}
{wordIndex < line.words.length - 1 ? ' ' : ''}
</Text>
)
})}
</Text>
)
})}
</View>
</View>
)
}
const prev = showContext && currentLineIdx > 0 ? lines[currentLineIdx - 1]?.text : ''
const curr = currentLineIdx >= 0 ? lines[currentLineIdx]?.text : lines[0]?.text
+4 -1
View File
@@ -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 }) => {
<KaraokeLyrics
alignedWords={alignedWords}
currentTimeS={(progressInfo?.pos || 0) / 1000}
mode="teleprompter"
teleprompterLines={6}
teleprompterAnchorLine={1}
/>
) : (
<Text
+8 -2
View File
@@ -999,13 +999,19 @@ const RecordPlayback = ({ route }) => {
position: 'absolute',
left: 0,
right: 0,
bottom: bottom + 180,
top: top + 90,
paddingHorizontal: gutters,
}}
>
<CreateLyricsHeader>
{isRecording && alignedWords.length > 0 ? (
<KaraokeLyrics alignedWords={alignedWords} currentTimeS={pos} />
<KaraokeLyrics
alignedWords={alignedWords}
currentTimeS={pos}
mode="teleprompter"
teleprompterLines={6}
teleprompterAnchorLine={1}
/>
) : (
<Text
style={{