feat: fix lecteur

This commit is contained in:
2026-03-03 14:19:06 +01:00
parent 787d76c5b2
commit dbd52fa285
2 changed files with 298 additions and 697 deletions
File diff suppressed because it is too large Load Diff
+127
View File
@@ -0,0 +1,127 @@
/**
* lyricsUtils.js
* Logique de parsing des paroles synchronisées (Karaoké)
*/
const SECTION_TAG_REGEX = /^\s*\[([^\]]+)\]\s*$/i
const SECTION_TAG_LEADING_REGEX = /^\s*\[([^\]]+)\]\s*/i
const cleanText = (txt) =>
String(txt || '')
.replace(/\s+/g, ' ')
.trim()
const isSentenceEnd = (txt) => /[.!?]$/.test((txt || '').trim())
/**
* Normalise les noms de sections (Chorus -> Refrain, etc.)
*/
const getSectionLabel = (rawLabel, type, index) => {
const lower = rawLabel.toLowerCase()
if (lower.includes('refrain') || lower.includes('chorus'))
return `Refrain ${index > 1 ? index : ''}`
if (lower.includes('couplet') || lower.includes('verse'))
return `Couplet ${index > 1 ? index : ''}`
if (lower.includes('intro')) return 'Intro'
if (lower.includes('pont') || lower.includes('bridge')) return 'Pont'
return rawLabel || `Section ${index}`
}
/**
* Détecte le type de section pour le filtrage
*/
const getSectionType = (label) => {
const l = label.toLowerCase()
if (l.includes('refrain') || l.includes('chorus')) return 'refrain'
if (l.includes('couplet') || l.includes('verse')) return 'couplet'
return 'other'
}
/**
* Transforme une liste de mots synchronisés en structure Sections > Lignes > Mots
*/
export const processKaraokeSections = (alignedWords = []) => {
if (!Array.isArray(alignedWords) || alignedWords.length === 0) return []
const sections = []
let currentSection = null
let currentLineWords = []
let sectionCounts = {}
let globalLineIdx = 0
const flushSection = () => {
if (currentSection) {
// On finit la dernière ligne si nécessaire
if (currentLineWords.length > 0) {
currentSection.lines.push({
words: [...currentLineWords],
startS: currentLineWords[0].startS,
endS: currentLineWords[currentLineWords.length - 1].endS,
globalIdx: globalLineIdx++,
})
currentLineWords = []
}
if (currentSection.lines.length > 0) {
sections.push(currentSection)
}
}
}
const startNewSection = (rawTag) => {
flushSection()
const type = getSectionType(rawTag)
sectionCounts[type] = (sectionCounts[type] || 0) + 1
currentSection = {
key: `section-${sections.length}`,
label: getSectionLabel(rawTag, type, sectionCounts[type]),
type: type,
lines: [],
}
}
for (let i = 0; i < alignedWords.length; i++) {
const w = alignedWords[i]
let wordText = String(w.word || '')
// 1. Détection de tag de section [Couplet]
const tagMatch = wordText.match(SECTION_TAG_LEADING_REGEX)
if (tagMatch) {
startNewSection(tagMatch[1])
wordText = wordText.replace(SECTION_TAG_LEADING_REGEX, '')
}
if (!currentSection) startNewSection('Musique')
const cleaned = cleanText(wordText)
if (!cleaned) continue
const wordObj = {
word: cleaned,
startS: Number(w.startS || 0),
endS: Number(w.endS || 0),
}
currentLineWords.push(wordObj)
// 2. Détection de fin de ligne (Ponctuation, saut de ligne ou pause > 0.8s)
const nextWord = alignedWords[i + 1]
const hasPause = nextWord && nextWord.startS - w.endS > 0.8
const hasNewline = String(w.word).includes('\n')
const hasPunctuation = isSentenceEnd(cleaned)
if (hasPause || hasNewline || hasPunctuation || i === alignedWords.length - 1) {
currentSection.lines.push({
words: [...currentLineWords],
startS: currentLineWords[0].startS,
endS: currentLineWords[currentLineWords.length - 1].endS,
globalIdx: globalLineIdx++,
})
currentLineWords = []
}
}
flushSection()
// On ne garde que les couplets et refrains pour l'affichage propre
return sections.filter((s) => s.type !== 'other' || s.label === 'Musique')
}