Files
BYW-VoiceHub/app/utils/lyric/lyricFormat.ts
T
Henry_Du 4ce4649aaf
Build VoiceHub FPK / build-fpk (push) Failing after 56m5s
Build and Push Docker Image / build-and-push (push) Canceled after 1h9m35s
初次提交
2026-09-06 08:19:02 +08:00

231 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { LyricLine } from '@applemusic-like-lyrics/lyric'
import { stripLyricMetadata, type StripOptions } from './lyricStripper'
import { keywords as defaultKeywords, regexes as defaultRegexes } from './exclude'
import { cloneDeep } from 'lodash-es'
/**
* 获取替换配置
*/
const getReplacementConfig = (settings: any) => {
const preset = settings.bracketReplacementPreset.value || 'dash'
const custom = settings.customBracketReplacement.value || '-'
let startStr = ' - '
const endStr = ' '
const isEnclosure = false
// 预设模式
if (preset === 'angleBrackets') {
return { startStr: '〔', endStr: '〕', isEnclosure: true }
} else if (preset === 'cornerBrackets') {
return { startStr: '「', endStr: '」', isEnclosure: true }
} else if (preset === 'custom') {
const trimmed = custom.trim()
// 启发式判断:如果是两个不同字符且不包含减号,视为成对符号 (如 "()")
const isPair = trimmed.length === 2 && trimmed[0] !== trimmed[1] && !trimmed.includes('-')
if (isPair) {
return { startStr: trimmed[0], endStr: trimmed[1], isEnclosure: true }
} else {
// 否则视为分隔符 (如 "-"),加上空格规范化
startStr = ' ' + trimmed + ' '
startStr = startStr.replace(/\s+/g, ' ')
return { startStr, endStr: ' ', isEnclosure: false }
}
} else if (preset === 'space') {
return { startStr: ' ', endStr: ' ', isEnclosure: false }
}
// 默认 dash 模式
return { startStr, endStr, isEnclosure }
}
/**
* 应用括号替换逻辑
*/
const applyBracketReplacement = (lines: LyricLine[], settings: any): LyricLine[] => {
if (!settings.replaceLyricBrackets.value) return lines
const { startStr, endStr, isEnclosure } = getReplacementConfig(settings)
// 正则:匹配全行括号 (如 "(Music)")
const fullBracketRegex = /^\s*[((][^()()]*[))]\s*$/
// 正则:匹配左括号
const leftBracketRegex = /[((]/g
// 正则:匹配右括号
const rightBracketRegex = /[))]/g
/**
* 处理普通字符串 (用于翻译和罗马音)
*/
const processString = (str: string): string => {
if (!str) return str
// 如果是全行括号且非闭合模式 (如分隔符模式),则直接去除括号
if (!isEnclosure && fullBracketRegex.test(str)) {
return str
.replace(/^\s*[((]/, '')
.replace(/[))]\s*$/, '')
.trim()
}
let res = str.replace(leftBracketRegex, startStr)
if (isEnclosure) {
res = res.replace(rightBracketRegex, endStr)
} else {
// 分隔符模式:右括号如果是结尾则移除,否则替换为结束符(空格)
res = res.replace(/[))](?=\s*$)/g, '').replace(rightBracketRegex, endStr)
// 清理重复分隔符 (如 " - - " -> " - ")
if (startStr.includes('-')) {
res = res.replace(/(?:\s*-\s*){2,}/g, ' - ')
}
}
return res
}
return lines.map((line) => {
// 处理 words
if (line.words) {
const fullText = line.words.map((w) => w.word).join('')
const isFullBracket = fullBracketRegex.test(fullText)
// 整行是括号内容,且处于分隔符模式 (非闭合) -> 直接移除首尾括号,不加分隔符
if (isFullBracket && !isEnclosure) {
let removedStart = false
let removedEnd = false
// 移除第一个左括号
for (const word of line.words) {
if (!removedStart && leftBracketRegex.test(word.word)) {
word.word = word.word.replace(leftBracketRegex, '')
removedStart = true
}
}
// 移除最后一个右括号
for (let i = line.words.length - 1; i >= 0; i--) {
const word = line.words[i]
if (!removedEnd && rightBracketRegex.test(word.word)) {
const lastIndex = Math.max(word.word.lastIndexOf(')'), word.word.lastIndexOf(')'))
if (lastIndex !== -1) {
word.word = word.word.substring(0, lastIndex) + word.word.substring(lastIndex + 1)
removedEnd = true
}
}
}
} else {
// 普通替换逻辑
line.words.forEach((word, index) => {
// 替换左括号
word.word = word.word.replace(leftBracketRegex, startStr)
if (isEnclosure) {
// 闭合模式:直接替换右括号
word.word = word.word.replace(rightBracketRegex, endStr)
} else {
// 分隔符模式:
word.word = word.word.replace(rightBracketRegex, (_, offset, str) => {
// 如果右括号在单词末尾,且是整句的最后一个单词 -> 移除 (视为句尾闭合)
const isAtWordEnd = offset === str.length - 1
const isAtLineEnd = index === line.words.length - 1
if (isAtWordEnd) {
return isAtLineEnd ? '' : endStr
}
return endStr
})
}
})
// 清理逻辑 (仅针对带减号的分隔符模式)
if (!isEnclosure && startStr.includes('-')) {
line.words.forEach((word, index) => {
// 单词内重复: " - - " -> " - "
word.word = word.word.replace(/(?:\s*-\s*){2,}/g, ' - ')
// 单词间重复: 前一个词以 "- " 结尾,当前词以 " -" 开头 -> 移除前一个的尾部
if (index > 0) {
const prev = line.words[index - 1]
if (/-\s*$/.test(prev.word) && /^\s*-/.test(word.word)) {
prev.word = prev.word.replace(/-\s*$/, '')
// 确保当前词开头有完整分隔符
if (!/^\s*-\s+/.test(word.word)) {
word.word = ' - ' + word.word.replace(/^\s*-\s*/, '')
}
}
}
})
}
}
}
// 处理翻译和罗马音
if (line.translatedLyric) line.translatedLyric = processString(line.translatedLyric)
if (line.romanLyric) line.romanLyric = processString(line.romanLyric)
return line
})
}
/**
* 格式化歌词
* 主要处理括号替换、关键词过滤等
* @param lines 歌词行
* @param settings 歌词设置对象 (从 useLyricSettings 获取)
* @param metadata 可选的歌曲元数据,用于更精确的歌词清洗
*/
export const formatLyric = (
lines: LyricLine[],
settings: any,
metadata?: { title?: string; artists?: string[] }
): LyricLine[] => {
const { enableExcludeLyrics, excludeLyricsUserKeywords, excludeLyricsUserRegexes } = settings
if (!lines || lines.length === 0) return []
// 使用 cloneDeep 避免修改原始引用 (如果需要)
// 但 formatLyric 通常被视为纯函数处理流程,这里我们操作的是 parsedLyrics 的结果
// 为了安全起见,克隆一份
let processedLines = cloneDeep(lines)
// 1. 智能元数据清洗 (Smart Metadata Stripping)
if (enableExcludeLyrics && enableExcludeLyrics.value) {
const userKeywords = excludeLyricsUserKeywords.value || []
const userRegexes = excludeLyricsUserRegexes.value || []
const mergedKeywords = [...new Set([...defaultKeywords, ...userKeywords])]
const mergedRegexes = [...new Set([...defaultRegexes, ...userRegexes])]
const options: StripOptions = {
keywords: mergedKeywords,
regexPatterns: mergedRegexes,
matchMetadata: metadata
}
processedLines = stripLyricMetadata(processedLines, options)
}
// 2. 基础过滤 (保留)
processedLines = processedLines.filter((line) => {
const text = line.words.map((w) => w.word).join('')
const transText = line.translatedLyric || ''
if (!text.trim()) {
if (!transText.trim() || transText.includes('著作权') || transText.includes('QQ音乐')) {
return false
}
}
if (text.includes('著作权') || text.includes('QQ音乐')) return false
if (transText.includes('著作权') || transText.includes('QQ音乐') || transText.trim() === '//') {
line.translatedLyric = ''
}
return true
})
// 3. 括号替换
processedLines = applyBracketReplacement(processedLines, settings)
return processedLines
}