[O] AI retry
This commit is contained in:
+21
-7
@@ -50,16 +50,30 @@ const getPlaylistRaw = cached('playlists_raw',
|
||||
function normalizeTimestamps(text: string): string {
|
||||
// Replace all [dd:dd:dd] wit [dd:dd.dd]
|
||||
return text.replace(/\[(\d+):(\d+):(\d+)\]/g, '[$1:$2.$3]')
|
||||
.replace(/\[(\d+):(\d+)\]/g, '[$1:$2.00]')
|
||||
}
|
||||
|
||||
interface NeteaseLyricsResponse { lrc: { lyric: string }, lang: string }
|
||||
const getLyricsRaw = cached('lyrics_raw',
|
||||
async (songId: number) => {
|
||||
const raw = (await ne.lyric({ id: songId })).body as any as NeteaseLyricsResponse
|
||||
const lang = franc(raw.lrc.lyric.replace(/\[.*?\]/g, '').replace(/\s+/g, ' ').trim())
|
||||
raw.lrc.lyric = normalizeTimestamps(raw.lrc.lyric)
|
||||
return { ...raw, lang }
|
||||
})
|
||||
const _getLyricsRaw = cached('lyrics_raw',
|
||||
async (songId: number) => {
|
||||
const raw = (await ne.lyric({ id: songId })).body as any as NeteaseLyricsResponse
|
||||
const lang = franc(raw.lrc.lyric.replace(/\[.*?\]/g, '').replace(/\s+/g, ' ').trim())
|
||||
return { ...raw, lang }
|
||||
})
|
||||
|
||||
const getLyricsRaw = async (songId: number): Promise<NeteaseLyricsResponse & { lang: string }> => {
|
||||
const raw = await _getLyricsRaw(songId)
|
||||
raw.lrc.lyric = normalizeTimestamps(raw.lrc.lyric)
|
||||
// Remove lines in the beginning of the lyrics that follow the pattern /\[.+\].+[::].+/ until the first line that doesn't match
|
||||
const lines = raw.lrc.lyric.split('\n')
|
||||
let startIndex = 0
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (/^\[.+\].+[::].+/.test(lines[i])) startIndex = i + 1
|
||||
else break
|
||||
}
|
||||
raw.lrc.lyric = lines.slice(startIndex).join('\n')
|
||||
return raw
|
||||
}
|
||||
|
||||
export const getSongRaw = cached('songs_raw',
|
||||
async (songId: number) => {
|
||||
|
||||
@@ -26,12 +26,11 @@ const req = {
|
||||
このテキストを解析し、指定された形式の「ふりがな・読み仮名付きテキスト」に変換してください。
|
||||
|
||||
処理ルール
|
||||
1. **メタデータの除外**: "作词"、"作曲"、"编曲"など、歌詞以外の情報を含む行はすべて除外してください。
|
||||
2. **出力形式とタイムスタンプ**: 1行につき \`[タイムスタンプ] ふりがな付き歌詞\` の形式で出力してください。
|
||||
1. **出力形式とタイムスタンプ**: 1行につき \`[タイムスタンプ] ふりがな付き歌詞\` の形式で出力してください。
|
||||
* **重要**: タイムスタンプの形式(例: \`[mm:ss.sss]\`)や数値は一切変更せず、入力されたままの状態で維持してください。
|
||||
3. **ふりがな(漢字)**: 歌詞本文では、**全ての漢字**に \`漢字(ふりがな)\` の形式でふりがな(ルビ)を付けてください。
|
||||
4. **読み仮名(非日本語)**: 英語などの非日本語の単語には、\`Word(カタカナ読み)\` の形式でカタカナの読み仮名を付けてください。(例: \`Good(グッド)\`)。
|
||||
5. **その他**: ひらがな、カタカナ、句読点などはそのまま出力してください。
|
||||
2. **ふりがな(漢字)**: 歌詞本文では、**全ての漢字**に \`漢字(ふりがな)\` の形式でふりがな(ルビ)を付けてください。
|
||||
3. **読み仮名(非日本語)**: 英語などの非日本語の単語には、\`Word(カタカナ読み)\` の形式でカタカナの読み仮名を付けてください。(例: \`Good(グッド)\`)。
|
||||
4. **その他**: ひらがな、カタカナ、句読点などはそのまま出力してください。
|
||||
|
||||
重要
|
||||
* **全ての漢字および非日本語単語に読み仮名を付けてください。** 読み方が不明な場合でも、文脈から最も一般的だと思われる読み方を付けてください。漢字をそのまま残してはいけません。
|
||||
@@ -49,10 +48,7 @@ const req = {
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: `[00:00.000] 作词 : 椎名林檎
|
||||
[00:01.000] 作曲 : 椎名林檎
|
||||
[00:02.000] 编曲 : 亀田誠治
|
||||
[00:15.570]蝉の声を聞く度に
|
||||
content: `[00:15.570]蝉の声を聞く度に
|
||||
[00:19.270]目に浮かぶ九十九里浜
|
||||
[00:22.600]皺々の祖母の手を離れ
|
||||
[00:25.880]独りで訪れた歓楽街
|
||||
@@ -72,7 +68,13 @@ const req = {
|
||||
[01:24.480]女王と云う肩書きを誇らしげに掲げる
|
||||
[01:58.370]女に成ったあたしが
|
||||
[02:01.950]売るのは自分だけで
|
||||
[02:05.440]同情を欲したときに`
|
||||
[02:05.440]同情を欲したときに
|
||||
[02:08.790]全てを失うだろう
|
||||
[02:12.720]JR新宿駅の東口を出たら
|
||||
[02:19.740]其処はあたしの庭
|
||||
[02:23.030]大遊戯場歌舞伎町
|
||||
[02:33.740]今夜からは此の町で
|
||||
[02:39.300]娘のあたしが女王`
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
@@ -96,7 +98,13 @@ const req = {
|
||||
[01:24.480] 女王(じょおう)と 云(い)う 肩書(かたが)きを 誇(ほこ)らしげに 掲(かか)げる
|
||||
[01:58.370] 女(おんな)に 成(な)ったあたしが
|
||||
[02:01.950] 売(う)るのは 自分(じぶん)だけで
|
||||
[02:05.440] 同情(どうじょう)を 欲(ほ)したときに`
|
||||
[02:05.440] 同情(どうじょう)を 欲(ほ)したときに
|
||||
[02:08.790] 全(すべ)てを 失(うしな)うだろう
|
||||
[02:12.720] JR 新宿駅(しんじゅくえき)の 東口(ひがしぐち)を 出(で)たら
|
||||
[02:19.740] 其処(そこ)はあたしの 庭(にわ)
|
||||
[02:23.030] 大遊戯場(だいゆうぎじょう) 歌舞伎町(かぶきちょう)
|
||||
[02:33.740] 今夜(こんや)からは 此(こ)の 町(まち)で
|
||||
[02:39.300] 娘(むすめ)のあたしが 女王(じょおう)`
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
@@ -150,7 +158,7 @@ function parseFuriganaText(text: string): LyricLine[] {
|
||||
if (kanji && furigana) {
|
||||
let splitIndex = -1
|
||||
for (let i = kanji.length - 1; i >= 0; i--) {
|
||||
if (!isKanji(kanji[i])) {
|
||||
if (!isKanji(kanji[i]) && !('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'.includes(kanji[i]))) {
|
||||
splitIndex = i
|
||||
break
|
||||
}
|
||||
@@ -178,7 +186,7 @@ function parseFuriganaText(text: string): LyricLine[] {
|
||||
* Let the AI parse the raw lyrics into furigana-formatted text,
|
||||
* then use the local parser to convert that text into LyricLine[] structure.
|
||||
*/
|
||||
export async function aiParseLyricsRaw(raw: string): Promise<LyricLine[]> {
|
||||
export async function aiParseLyricsRaw(raw: string, tries: number = 5): Promise<LyricLine[]> {
|
||||
const thisReq = JSON.parse(JSON.stringify(req))
|
||||
thisReq.messages[thisReq.messages.length - 1].content = raw
|
||||
|
||||
@@ -190,11 +198,29 @@ export async function aiParseLyricsRaw(raw: string): Promise<LyricLine[]> {
|
||||
console.log('AI request:\n', raw)
|
||||
console.log('AI response:\n', responseText)
|
||||
console.log(`Finish reason: ${response.choices[0].finish_reason}`)
|
||||
|
||||
async function fail() {
|
||||
if (tries > 0) {
|
||||
console.warn(`Retrying AI parsing, ${tries} tries left...`)
|
||||
return await aiParseLyricsRaw(raw, tries - 1)
|
||||
}
|
||||
throw new Error('AI parsing failed after multiple attempts.')
|
||||
}
|
||||
|
||||
// If response does not contain any timestamp, something is wrong
|
||||
if (!/\[\d+:\d+\.\d+\]/.test(responseText)) {
|
||||
console.error('AI response does not contain any timestamps, indicating a failure. Request:', raw)
|
||||
console.error('AI response text:', responseText)
|
||||
throw new Error('AI response parsing failed, no timestamps found.')
|
||||
await fail()
|
||||
}
|
||||
|
||||
// If response doesn't contain a similar number of lines (+-3), something is wrong
|
||||
const inputLineCount = raw.split('\n').filter(line => line.trim() !== '').length
|
||||
const outputLineCount = responseText.split('\n').filter(line => line.trim() !== '').length
|
||||
if (Math.abs(inputLineCount - outputLineCount) > 3) {
|
||||
console.error(`AI response line count (${outputLineCount}) differs significantly from input (${inputLineCount}). Request:`, raw)
|
||||
console.error('AI response text:', responseText)
|
||||
await fail()
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -208,12 +234,13 @@ export async function aiParseLyricsRaw(raw: string): Promise<LyricLine[]> {
|
||||
}
|
||||
|
||||
export async function aiParseLyrics(raw: string): Promise<LyricLine[]> {
|
||||
// Split into maximum 20 lines per request
|
||||
const lines = raw.split('\n').filter(line => line.trim() !== '')
|
||||
const chunks: string[] = []
|
||||
for (let i = 0; i < lines.length; i += 20) {
|
||||
chunks.push(lines.slice(i, i + 20).join('\n'))
|
||||
}
|
||||
const results = await Promise.all(chunks.map(aiParseLyricsRaw))
|
||||
return results.flat()
|
||||
// Split into maximum n lines per request
|
||||
const n = 30
|
||||
const lines = raw.split('\n').filter(line => line.trim() !== '')
|
||||
const chunks: string[] = []
|
||||
for (let i = 0; i < lines.length; i += n) {
|
||||
chunks.push(lines.slice(i, i + n).join('\n'))
|
||||
}
|
||||
const results = await Promise.all(chunks.map(it => aiParseLyricsRaw(it, 5)))
|
||||
return results.flat()
|
||||
}
|
||||
@@ -48,7 +48,7 @@
|
||||
<div class="m3-font-headline-small">歌曲列表</div>
|
||||
<div class="m3-font-label-small pb-3px">{songs.length} 首歌曲</div>
|
||||
</div>
|
||||
<div class="vbox gap-12px mt-12px min-h-0 overflow-y-auto">
|
||||
<div class="vbox gap-12px mt-12px pb-12px min-h-0 flex-shrink-1 overflow-y-auto">
|
||||
{#each songs as song, index}
|
||||
<a href="/song/{song.id}" class="p-content">
|
||||
<SongInfo info={song} />
|
||||
|
||||
@@ -14,7 +14,9 @@ export function processLrcLine(line: LyricSegment[]): ProcLrcLine {
|
||||
}
|
||||
|
||||
// Fuzzy matching rules
|
||||
const fuzzyMatch = [['わ', 'は'], ['を', 'お']]
|
||||
const fuzzyMatch = [['わ', 'は'], ['を', 'お'], ['ず', 'づ'],
|
||||
['ぁ', 'あ'], ['ぃ', 'い'], ['ぅ', 'う'], ['ぇ', 'え'], ['ぉ', 'お'],
|
||||
['ゃ', 'や'], ['ゅ', 'ゆ'], ['ょ', 'よ'], ['っ', 'つ']]
|
||||
export function fuzzyEquals(kana1: string, kana2: string): string {
|
||||
[kana1, kana2] = [toHiragana(kana1), toHiragana(kana2)]
|
||||
if (kana1 === kana2) return 'right'
|
||||
|
||||
Reference in New Issue
Block a user