初次提交
Build VoiceHub FPK / build-fpk (push) Failing after 56m5s
Build and Push Docker Image / build-and-push (push) Canceled after 1h9m35s

This commit is contained in:
2026-09-06 08:19:02 +08:00
commit 4ce4649aaf
1061 changed files with 337258 additions and 0 deletions
+428
View File
@@ -0,0 +1,428 @@
import { computed, ref } from 'vue'
// 歌词行接口
export interface ParsedLyricLine {
time: number // 时间戳(毫秒)
content: string // 歌词内容
words?: Array<{
// 逐字歌词(可选)
time: number
duration: number
content: string
}>
}
// 歌词数据接口
export interface LyricData {
lrc: string // 普通歌词
trans?: string // 翻译歌词
yrc?: string // 逐字歌词
}
export const useLyrics = () => {
// 响应式状态
const currentLyrics = ref<ParsedLyricLine[]>([])
const translationLyrics = ref<ParsedLyricLine[]>([])
const wordByWordLyrics = ref<ParsedLyricLine[]>([])
const currentLyricIndex = ref(0)
const currentTime = ref(0)
const isLoading = ref(false)
const error = ref<string | null>(null)
// 歌词显示设置
const showTranslation = ref(false)
const showWordByWord = ref(false)
// 解析LRC格式歌词
const parseLrcLyrics = (lrcText: string): ParsedLyricLine[] => {
if (!lrcText) return []
const lines = lrcText.split('\n')
const lyrics: ParsedLyricLine[] = []
for (const line of lines) {
// 匹配时间标签 [mm:ss.xx] 或 [mm:ss:xx]
const timeMatch = line.match(/\[(\d{2}):(\d{2})[.:](\d{2,3})\](.*)/)
if (timeMatch) {
const minutes = parseInt(timeMatch[1])
const seconds = parseInt(timeMatch[2])
const milliseconds = parseInt(timeMatch[3].padEnd(3, '0'))
const content = timeMatch[4].trim()
const time = minutes * 60 * 1000 + seconds * 1000 + milliseconds
if (content) {
// 只添加有内容的歌词行
lyrics.push({ time, content })
}
}
}
return lyrics.sort((a, b) => a.time - b.time)
}
// 解析网易云逐字歌词
const parseYrcLyrics = (yrcText: string): ParsedLyricLine[] => {
if (!yrcText) return []
const lines = yrcText.split('\n')
const lyrics: ParsedLyricLine[] = []
for (const line of lines) {
try {
// 网易云逐字歌词格式:{"t":12420,"c":[{"tx":"ya ","t":290},{"tx":"la ","t":380}]}
if (line.startsWith('{')) {
const data = JSON.parse(line)
if (typeof data.t === 'number' && Array.isArray(data.c)) {
const start = data.t
let accTime = start
const words = data.c.map((word: any) => {
const d = typeof word.t === 'number' ? word.t : 0 // t 为毫秒时长
const wContent = typeof word.tx === 'string' ? word.tx : ''
const wTime = accTime
accTime += d
return {
time: wTime,
duration: d,
content: wContent
}
})
const content = data.c
.map((word: any) => (typeof word.tx === 'string' ? word.tx : ''))
.join('')
lyrics.push({
time: start,
content,
words
})
}
}
// 方括号 + 括号 的逐字格式
else if (line.includes('[')) {
// 示例 A(绝对时间在括号前):[12420,3470](12420,290,0)ya (12710,380,0)la
// 示例 B(相对时间在括号后):[0,4690]嘘(0,293)つ(293,293)き(586,293)
const timeMatch = line.match(/\[(\d+),(\d+)\](.*)/)
if (!timeMatch) continue
const startTime = parseInt(timeMatch[1])
const duration = parseInt(timeMatch[2])
const wordsText = timeMatch[3]
const words: Array<{ time: number; duration: number; content: string }> = []
// 解析示例 B:字(相对或绝对时间, 时长) —— 单位均为毫秒
for (const wordMatch of wordsText.matchAll(/([^()]+)\((\d+),(\d+)\)/g)) {
const content = wordMatch[1]
const t = parseInt(wordMatch[2])
const durMs = parseInt(wordMatch[3])
// 如果时间小于行起始时间,视为相对时间;否则视为绝对时间
const absTime = startTime > 0 && t < startTime ? startTime + t : t
words.push({
time: absTime,
duration: durMs,
content
})
}
// 若上面未匹配到,则解析示例 A: (绝对时间, 时长, 0)字 —— 单位为毫秒
if (words.length === 0) {
for (const wordMatch of wordsText.matchAll(/\((\d+),(\d+),\d+\)([^()]*)/g)) {
const absTime = parseInt(wordMatch[1])
const durMs = parseInt(wordMatch[2])
const content = wordMatch[3]
words.push({
time: absTime,
duration: durMs,
content
})
}
}
const content = words.map((w) => w.content).join('')
if (content.length > 0) {
lyrics.push({
time: startTime,
content,
words
})
}
}
} catch (e) {
console.warn('解析逐字歌词行失败:', line, e)
}
}
return lyrics.sort((a, b) => a.time - b.time)
}
// 统一调度后,移除旧的备用源歌词获取逻辑
// 统一调度后,移除旧的网易云逐字歌词解析函数(现统一用 parseYrcLyrics)
// 获取歌词(统一调度:先备用源,再vkeys)
const fetchLyrics = async (platform: string, musicId: string): Promise<void> => {
if (!platform || !musicId) {
console.error('fetchLyrics 参数错误:', { platform, musicId })
error.value = '缺少必要参数'
currentLyrics.value = []
translationLyrics.value = []
wordByWordLyrics.value = []
currentLyricIndex.value = 0
notifyHarmonyOSLyricsUpdate('')
return
}
isLoading.value = true
error.value = null
currentLyrics.value = []
translationLyrics.value = []
wordByWordLyrics.value = []
currentLyricIndex.value = 0
notifyHarmonyOSLyricsUpdate('')
try {
const { getLyrics } = useMusicSources()
const result = await getLyrics(platform as 'netease' | 'tencent', musicId)
if (result.success && result.data) {
const lyricData = result.data
// 优先使用逐字歌词
if (lyricData.yrc) {
wordByWordLyrics.value = parseYrcLyrics(lyricData.yrc)
currentLyrics.value = wordByWordLyrics.value
} else if (lyricData.lrc) {
currentLyrics.value = parseLrcLyrics(lyricData.lrc)
} else {
currentLyrics.value = []
}
// 翻译歌词
if (lyricData.trans) {
translationLyrics.value = parseLrcLyrics(lyricData.trans)
showTranslation.value = true
} else {
translationLyrics.value = []
showTranslation.value = false
}
const harmonyLyrics = getFormattedLyricsForHarmonyOS()
notifyHarmonyOSLyricsUpdate(harmonyLyrics)
error.value = null
isLoading.value = false
return
} else {
throw new Error(result.error || '未获取到歌词')
}
} catch (e: any) {
console.error('获取歌词失败:', e?.message || e)
error.value = e?.message || '获取歌词失败'
currentLyrics.value = []
translationLyrics.value = []
wordByWordLyrics.value = []
currentLyricIndex.value = 0
notifyHarmonyOSLyricsUpdate('')
} finally {
isLoading.value = false
}
}
// 通知鸿蒙侧歌词更新的辅助函数
const notifyHarmonyOSLyricsUpdate = (lyrics: string) => {
if (typeof window !== 'undefined' && window.HarmonyOS && window.HarmonyOS.postMessage) {
try {
const message = {
method: 'updateLyrics',
parameters: {
lyrics: lyrics
}
}
window.HarmonyOS.postMessage(JSON.stringify(message))
// 通知鸿蒙侧更新歌词
} catch (error) {
console.error('通知鸿蒙侧更新歌词失败:', error)
}
} else {
console.warn('鸿蒙侧消息接口不可用,无法更新歌词')
}
}
// 根据当前时间更新歌词索引
const updateCurrentLyricIndex = (time: number): void => {
currentTime.value = time
const lyrics = currentLyrics.value
if (lyrics.length === 0) {
// 没有歌词数据
return
}
// 找到当前时间对应的歌词行
for (let i = 0; i < lyrics.length; i++) {
if (time >= lyrics[i].time && (i === lyrics.length - 1 || time < lyrics[i + 1].time)) {
if (currentLyricIndex.value !== i) {
// 歌词索引更新
currentLyricIndex.value = i
}
return
}
}
// 未找到匹配的歌词行
}
// 获取当前歌词内容
const currentLyricContent = computed(() => {
const lyrics = currentLyrics.value
if (lyrics.length === 0 || currentLyricIndex.value >= lyrics.length) {
return ''
}
return lyrics[currentLyricIndex.value].content
})
// 获取当前翻译歌词
const currentTranslationContent = computed(() => {
if (!showTranslation.value || translationLyrics.value.length === 0) {
return ''
}
const currentTime = currentLyrics.value[currentLyricIndex.value]?.time
if (!currentTime) return ''
// 找到对应时间的翻译歌词
for (let i = 0; i < translationLyrics.value.length; i++) {
if (
currentTime >= translationLyrics.value[i].time &&
(i === translationLyrics.value.length - 1 ||
currentTime < translationLyrics.value[i + 1].time)
) {
return translationLyrics.value[i].content
}
}
return ''
})
// 获取格式化的LRC歌词文本
const getFormattedLrcText = (): string => {
if (currentLyrics.value.length === 0) {
return '[00:00.00]暂无歌词'
}
return currentLyrics.value
.map((line) => {
const minutes = Math.floor(line.time / 60000)
const seconds = Math.floor((line.time % 60000) / 1000)
const milliseconds = Math.floor((line.time % 1000) / 10)
const timeStr = `[${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(2, '0')}]`
return `${timeStr}${line.content}`
})
.join('\r\n')
}
// 获取格式化的歌词用于鸿蒙侧,符合AVMetadata.lyric要求
const getFormattedLyricsForHarmonyOS = (): string => {
if (currentLyrics.value.length === 0) {
return '[00:00.00]暂无歌词'
}
// 按照鸿蒙侧要求格式化歌词:[mm:ss.xx]歌词内容\r\n
let formattedLyrics = currentLyrics.value
.map((line) => {
const minutes = Math.floor(line.time / 60000)
const seconds = Math.floor((line.time % 60000) / 1000)
const milliseconds = Math.floor((line.time % 1000) / 10)
// 格式:[mm:ss.xx]歌词内容
const timeStr = `[${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(2, '0')}]`
return `${timeStr}${line.content}`
})
.join('\r\n')
// 鸿蒙侧字符串长度限制:<40960字节
const maxBytes = 40960
const encoder = new TextEncoder()
// 检查字符串长度是否超过限制
if (encoder.encode(formattedLyrics).length > maxBytes) {
console.warn('歌词长度超过鸿蒙侧限制,正在截断...')
// 逐行添加歌词,直到接近字节限制
let truncatedLyrics = ''
for (const line of currentLyrics.value) {
const minutes = Math.floor(line.time / 60000)
const seconds = Math.floor((line.time % 60000) / 1000)
const milliseconds = Math.floor((line.time % 1000) / 10)
const timeStr = `[${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(2, '0')}]`
const lineText = `${timeStr}${line.content}\r\n`
// 检查添加这一行后是否会超过限制
const testLyrics = truncatedLyrics + lineText
if (encoder.encode(testLyrics).length > maxBytes - 100) {
// 留100字节缓冲
break
}
truncatedLyrics += lineText
}
formattedLyrics = truncatedLyrics.trim()
}
// 歌词格式化完成
return formattedLyrics
}
// 清空歌词
const clearLyrics = (): void => {
currentLyrics.value = []
translationLyrics.value = []
wordByWordLyrics.value = []
currentLyricIndex.value = 0
currentTime.value = 0
error.value = null
}
// 跳转到指定歌词行
const seekToLyricLine = (index: number): number => {
if (index >= 0 && index < currentLyrics.value.length) {
return currentLyrics.value[index].time / 1000 // 返回秒数
}
return 0
}
return {
// 状态
currentLyrics: readonly(currentLyrics),
translationLyrics: readonly(translationLyrics),
wordByWordLyrics: readonly(wordByWordLyrics),
currentLyricIndex: readonly(currentLyricIndex),
currentTime: readonly(currentTime),
isLoading: readonly(isLoading),
error: readonly(error),
// 设置
showTranslation,
showWordByWord,
// 计算属性
currentLyricContent,
currentTranslationContent,
// 方法
fetchLyrics,
updateCurrentLyricIndex,
getFormattedLrcText,
getFormattedLyricsForHarmonyOS, // 保持向后兼容
notifyHarmonyOSLyricsUpdate, // 鸿蒙侧歌词更新通知
clearLyrics,
seekToLyricLine,
parseLrcLyrics,
parseYrcLyrics
}
}