[O] Better lyrics
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
"@iconify-json/material-symbols": "^1.2.46",
|
||||
"@iconify-json/svg-spinners": "^1.2.4",
|
||||
"@neteasecloudmusicapienhanced/api": "^4.29.17",
|
||||
"@openrouter/sdk": "^0.1.17",
|
||||
"@unocss/core": "^66.5.6",
|
||||
"@unocss/extractor-svelte": "^66.5.6",
|
||||
"@unocss/preset-attributify": "^66.5.6",
|
||||
@@ -184,6 +185,8 @@
|
||||
|
||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||
|
||||
"@openrouter/sdk": ["@openrouter/sdk@0.1.17", "", { "dependencies": { "zod": "^3.25.0 || ^4.0.0" }, "peerDependencies": { "@tanstack/react-query": "^5", "react": "^18 || ^19", "react-dom": "^18 || ^19" }, "optionalPeers": ["@tanstack/react-query", "react", "react-dom"] }, "sha512-RFN0sfe83G85MirfpkZSuoX8hLLucemnwqrTr53vlrJmBJZ244CCnuZ33vpVUI8rLg+hP1i/smW6IExzYRDGDg=="],
|
||||
|
||||
"@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="],
|
||||
|
||||
"@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.1", "", { "os": "android", "cpu": "arm64" }, "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA=="],
|
||||
@@ -1074,6 +1077,8 @@
|
||||
|
||||
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
|
||||
|
||||
"zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="],
|
||||
|
||||
"@babel/generator/@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
|
||||
|
||||
"@babel/traverse/globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="],
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"@iconify-json/material-symbols": "^1.2.46",
|
||||
"@iconify-json/svg-spinners": "^1.2.4",
|
||||
"@neteasecloudmusicapienhanced/api": "^4.29.17",
|
||||
"@openrouter/sdk": "^0.1.17",
|
||||
"@unocss/core": "^66.5.6",
|
||||
"@unocss/extractor-svelte": "^66.5.6",
|
||||
"@unocss/preset-attributify": "^66.5.6",
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import OpenAI from 'openai'
|
||||
import { OpenRouter } from '@openrouter/sdk'
|
||||
import type { LyricLine, LyricSegment } from '../../../shared/types'
|
||||
import { isKanji } from 'wanakana'
|
||||
import { isKana, isKanji } from 'wanakana'
|
||||
|
||||
// Please put OPENAI_API_KEY in your environment variables.
|
||||
const client = new OpenAI()
|
||||
// Please put OPENROUTER_API_KEY in your environment variables.
|
||||
if (!process.env.OPENROUTER_API_KEY) throw new Error('Please set OPENROUTER_API_KEY in your environment variables.')
|
||||
const client = new OpenRouter({
|
||||
apiKey: process.env.OPENROUTER_API_KEY!
|
||||
})
|
||||
const req = {
|
||||
// model: "gpt-5.1-chat-latest",
|
||||
model: "gpt-4.1",
|
||||
model: "openai/gpt-5",
|
||||
// model: "gpt-4.1",
|
||||
// model: 'x-ai/grok-4.1-fast:free',
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
@@ -32,6 +36,9 @@ const req = {
|
||||
2. **ふりがな(漢字)**: 歌詞本文では、**全ての漢字**に \`漢字(ふりがな)\` の形式でふりがな(ルビ)を付けてください。
|
||||
3. **読み仮名(非日本語)**: 英語などの非日本語の単語には、\`Word(カタカナ読み)\` の形式でカタカナの読み仮名を付けてください。(例: \`Good(グッド)\`)。
|
||||
4. **その他**: ひらがな、カタカナ、句読点などはそのまま出力してください。
|
||||
5. **冗長なふりがなの禁止と混在処理(重要)**:
|
||||
* **カタカナ・ひらがなのみの単語**: 元からカタカナやひらがなで表記されている単語には、ふりがなを付けないでください(例: \`アニメ\` はそのまま \`アニメ\` とし、\`アニメ(アニメ)\` としない)。
|
||||
* **漢字とかなの混在**: \`のど飴\` のように漢字とかなが混ざっている表現は、全体を括るのではなく、**漢字部分にのみ**ふりがなを付けてください(例: \`のど 飴(あめ)\`)。 \`のど飴(のどあめ)\` のように、かな部分を含めてふりがなを振ることは禁止です。
|
||||
|
||||
重要
|
||||
* **全ての漢字および非日本語単語に読み仮名を付けてください。** 読み方が不明な場合でも、文脈から最も一般的だと思われる読み方を付けてください。漢字をそのまま残してはいけません。
|
||||
@@ -156,10 +163,11 @@ function parseFuriganaText(text: string): LyricLine[] {
|
||||
|
||||
while ((match = tokenRegex.exec(lyricText)) !== null) {
|
||||
const [, kanji, furigana, plain] = match
|
||||
if (kanji && furigana) {
|
||||
if (kanji && kanji == furigana) lyric.push(kanji)
|
||||
else if (kanji && furigana) {
|
||||
let splitIndex = -1
|
||||
for (let i = kanji.length - 1; i >= 0; i--) {
|
||||
if (!isKanji(kanji[i]) && !('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'.includes(kanji[i]))) {
|
||||
if (!isKanji(kanji[i]) && !isKana(kanji[i]) && !('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'.includes(kanji[i]))) {
|
||||
splitIndex = i
|
||||
break
|
||||
}
|
||||
@@ -191,14 +199,14 @@ export async function aiParseLyricsRaw(raw: string, tries: number = 5): Promise<
|
||||
const thisReq = JSON.parse(JSON.stringify(req))
|
||||
thisReq.messages[thisReq.messages.length - 1].content = raw
|
||||
|
||||
const response = await client.chat.completions.create(thisReq)
|
||||
const text = response.choices[0].message?.content || ''
|
||||
const response = await client.chat.send(thisReq)
|
||||
const text = response.choices[0].message?.content as string || ''
|
||||
|
||||
// Clean up potential markdown blocks
|
||||
const responseText = text.replace(/```/g, '').trim()
|
||||
console.log('AI request:\n', raw)
|
||||
console.log('AI response:\n', responseText)
|
||||
console.log(`Finish reason: ${response.choices[0].finish_reason}`)
|
||||
console.log(`Finish reason: ${response.choices[0].finishReason}`)
|
||||
|
||||
async function fail() {
|
||||
if (tries > 0) {
|
||||
@@ -236,11 +244,16 @@ export async function aiParseLyricsRaw(raw: string, tries: number = 5): Promise<
|
||||
|
||||
export async function aiParseLyrics(raw: string): Promise<LyricLine[]> {
|
||||
// Split into maximum n lines per request
|
||||
const n = 30
|
||||
const maxLines = 30
|
||||
const lines = raw.split('\n').filter(line => line.trim() !== '')
|
||||
if (lines.length === 0) return []
|
||||
|
||||
const numChunks = Math.ceil(lines.length / maxLines)
|
||||
const linesPerChunk = Math.ceil(lines.length / numChunks)
|
||||
|
||||
const chunks: string[] = []
|
||||
for (let i = 0; i < lines.length; i += n) {
|
||||
chunks.push(lines.slice(i, i + n).join('\n'))
|
||||
for (let i = 0; i < lines.length; i += linesPerChunk) {
|
||||
chunks.push(lines.slice(i, i + linesPerChunk).join('\n'))
|
||||
}
|
||||
const results = await Promise.all(chunks.map(it => aiParseLyricsRaw(it, 5)))
|
||||
return results.flat()
|
||||
|
||||
@@ -5,6 +5,6 @@ export const load: PageServerLoad = async ({ params }) => {
|
||||
const songId = +params.id
|
||||
const song = await getSongRaw(songId)
|
||||
const lrc = await getLyricsProcessed(songId)
|
||||
const audioUrl = await getSongUrl(songId)
|
||||
return { song, lrc, audioUrl }
|
||||
// const audioUrl = await getSongUrl(songId)
|
||||
return { song, lrc, audioUrl: undefined }
|
||||
}
|
||||
@@ -75,7 +75,7 @@
|
||||
if (!startTime && input) startTime = Date.now()
|
||||
console.log(`input changed: ${input}`)
|
||||
// Convert to hiragana
|
||||
inp = toHiragana(inp, { IMEMode: true })
|
||||
inp = toHiragana(inp.replaceAll(' ', ''), { IMEMode: true })
|
||||
const imeUsed = input !== inp
|
||||
|
||||
function findLoc() {
|
||||
@@ -167,6 +167,9 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onresize={() => caret && animateCaret(caret)} />
|
||||
|
||||
|
||||
<AppBar title={data.song.name} sub={artistAndAlbum(data.song)}>
|
||||
<MenuItem textIcon="あ" onclick={() => settings.isFuri = !settings.isFuri}>{settings.isFuri ? "隐藏" : "显示"}假名标注</MenuItem>
|
||||
<MenuItem textIcon="カ" onclick={() => settings.allKata = !settings.allKata}>{settings.allKata ? "恢复平假名" : "全部转换为片假名"}</MenuItem>
|
||||
|
||||
@@ -4,7 +4,8 @@ import type { LyricSegment } from "../../../shared/types";
|
||||
export type ProcLrcSeg = { swi: number, kanji?: string, kana: string }
|
||||
export type ProcLrcLine = { parts: ProcLrcSeg[], totalLen: number }
|
||||
export function processLrcLine(line: LyricSegment[]): ProcLrcLine {
|
||||
let result: any[] = line.map(part => (typeof part === "string" ? { kana: part } : { kanji: part[0], kana: part[1] }))
|
||||
let result: any[] = line.map(part => (typeof part === "string" ? { kana: part } :
|
||||
toHiragana(part[0]) === toHiragana(part[1]) ? { kana: part[0] } : { kanji: part[0], kana: part[1] }))
|
||||
let swi = 0
|
||||
for (let item of result) {
|
||||
item['swi'] = swi
|
||||
@@ -14,7 +15,7 @@ export function processLrcLine(line: LyricSegment[]): ProcLrcLine {
|
||||
}
|
||||
|
||||
// Fuzzy matching rules
|
||||
const fuzzyMatch = [['わ', 'は'], ['を', 'お'], ['ず', 'づ'],
|
||||
const fuzzyMatch = [['わ', 'は'], ['を', 'お'], ['ず', 'づ'], ['が', 'は'],
|
||||
['ぁ', 'あ'], ['ぃ', 'い'], ['ぅ', 'う'], ['ぇ', 'え'], ['ぉ', 'お'],
|
||||
['ゃ', 'や'], ['ゅ', 'ゆ'], ['ょ', 'よ'], ['っ', 'つ']]
|
||||
export function fuzzyEquals(kana1: string, kana2: string): string {
|
||||
|
||||
Reference in New Issue
Block a user