Skip to content

bug: #308 以降 歌詞解決が直列全探索で極端に遅く、有名曲を間欠的に取りこぼす #326

Description

@GeneralD

agent type regression impact platform

問題

#308(確信度ベースのメタデータ+歌詞解決)以降、実運用で 2 つの体感劣化が起きている。両者は独立の症状に見えるが、**どちらも「全候補 × 全ソース × 全 Tier を直列で試す」という #308設計#318 の per-call ephemeral session が噛み合った副作用**で、相互に悪化させ合っている。

症状 A — 未キャッシュ曲の歌詞表示が極端に遅い

以前よりも、キャッシュされていない曲の歌詞が出るまでがずっと遅くなった。曲の中盤くらいになってやっと表示されることもある。

症状 B — 有名曲を間欠的に取りこぼす

タイトル/アーティストはカバー曲でも原曲のものがきちんと取れていて一見精度が高そうに見えるのに、本来見つからないはずのない有名曲の歌詞を間欠的に取りこぼすようになった。

原因分析

原因 1(症状 A)— メタデータ・歌詞解決の全経路が直列全探索

#308 が「LLM が当たっても短絡せず全ソースを試す/全候補を全 Tier で試す」方針に倒したため、未キャッシュ曲では以下がすべて 直列 await で積み上がる。

段階 直列/並列 最悪時間
debounce 固定 0.3s
MetadataRepositoryImpl.resolve() LLM 直列 60s(#318 timeout)
同 MusicBrainz(内部 2 クエリ直列) 直列 10s × 2 = 20s
同 Regex 直列(ローカル) ~0s
fetchLyrics Tier A(LRCLIB get) 直列 × N 候補 10s × N
同 Tier B(LRCLIB search) 直列 × N 候補 10s × N
合計(候補 N=10, Tier C 未設定 約 280 秒(≈4 分 40 秒)

async let / withThrowingTaskGroup は使われておらず、MetadataRepositoryImpl.resolve() の 3 ソースも、fetchLyrics の候補ループもすべて逐次 await。候補数は区切り記号の多い曲名や MusicBrainz の複数レコーディングで 5〜15 件になり得るため、10s × N × 2 Tier がそのまま効く。「曲の中盤でやっと表示」という体感(一般的なポップスで 3〜5 分)はこの見積もりレンジと整合する。

原因 2(症状 B)— Tier A だけがバリデーション無しでキャッシュ書き込みする非対称

fetchLyrics(candidates:) は冒頭のキャッシュ read で 全行を無条件で再バリデートする(#308 が pre-#308 の poisoned row を握りつぶすために入れたもの)。ところが書き込み側は非対称で、Tier A(tierAExactMatch)だけが LyricsMatchValidator を通さずに store() する(Tier B/C は書き込み前に必ず isValid を通す)。

さらに LyricsMatchValidator.normalizedtext.lowercased().filter { $0.isLetter || $0.isNumber } で**空白・記号を全除去して連結**する方式のため、括弧書き・付記を特別扱いしない。短いタイトルに定型サフィックスが付くと簡単に閾値 0.6 を割る。

candidate  "Yesterday"                 → "yesterday"                (9)
LRCLIB     "Yesterday - Remastered 2009" → "yesterdayremastered2009" (24)
Levenshtein=15  類似度 = 1 - 15/24 = 0.375  → 0.6 未満で invalid

有名曲ほどリマスター/ライブ/別バージョン表記のカタログを持つため、無名曲より有名曲の方が引っかかりやすい非対称を持つ。

false negative のループ:

  1. 有名曲を初回再生 → Tier A が LRCLIB /api/get にヒットし、検証のまま閾値割れの行をキャッシュ書き込み。歌詞は直接 return されるので初回は正しく表示される(title/artist も原曲寄りで「精度が高そうに見える」)。
  2. 2 回目以降の再生 → read ループで同じ行がヒットするが validator.isValidfalsecontinue でキャッシュを握りつぶし、直列ライブ再取得(原因 1)に落ちる。
  3. ライブ再取得は毎回 10s タイムアウトのネットワーク往復。LRCLIB の一時的な 5xx / タイムアウト / レート制限に晒される。Tier C は fallback_command設定なら即 nil安全網にならない。
  4. 全 Tier が nilfetchLyricsnilUI.notFound(歌詞なし)。

つまり症状 B の実害は、原因 1 の「遅い直列全探索」があるからこそ、キャッシュを握りつぶすたびにネットワーク不調に晒されて顕在する。両者は同じ #308 設計から派生し、相互に増幅している。

補足表示と歌詞検索の経路分離

表示用タイトル/アーティストは常に candidates.first(=LLM 補正済みの綺麗な名前)を使う一方、歌詞取得はフル候補配列を別経路で処理する。これ自体は false negative の直接原因ではないが、「表示綺麗なのに歌詞が出ない」という体感のズレを作っている。

再現手順

  1. LLM([ai])を設定した状態で lyra daemon を起動
  2. キャッシュに無い曲(特にリマスター/ライブ表記のある有名曲)を再生する。
  3. 症状 A: 歌詞が出るまで数十秒〜数分待たされる(曲の中盤でやっと表示)。
  4. 同じ曲を後日もう一度再生する。
  5. 症状 B: 初回は出たのに、2 回目以降は間欠的に「歌詞なし」になる。

期待する動作

  • 未キャッシュ曲でも、候補群を**並列探索**し「最初にヒットしたものを採用、他はキャンセル」することで、最悪でも「最も遅い 1 回のタイムアウト」(数秒〜十数秒)で表示される。
  • 一度キャッシュに乗った有名曲は、read 時の再バリデーションで握りつぶされず、再生のたびに安定して表示される。

実際の動作

  • 未キャッシュ曲は直列全探索で最悪 4〜5 分待たされる。
  • Tier A が閾値割れの行を書くと、2 回目以降の再生で毎回キャッシュが握りつぶされ、ネットワーク不調のたびに歌詞なしへ退行する。

該当箇所

  • Sources/MetadataRepository/MetadataRepositoryImpl.swift:15-22 — LLM → MusicBrainz → Regex を async let/TaskGroup 無しで直列 await
  • Sources/MetadataDataSource/MusicBrainzMetadataDataSourceImpl.swift:35-45 — 2 クエリを直列ループ、両方失敗で 20s
  • Sources/LyricsRepository/LyricsRepositoryImpl.swift:39-47 — キャッシュ read が全候補直列 + 全行を無条件で再バリデート
  • Sources/LyricsRepository/LyricsRepositoryImpl.swift:64-74 — Tier A が validator.isValid通さず store()(Tier B: 88 行目 / Tier C: 106 行目は通している)
  • Sources/LyricsRepository/LyricsMatchValidator.swift:28-30normalized が空白・記号を全除去して連結(付記に弱い)/閾値 0.6(4 行目)
  • Sources/ScopedAPISession/ScopedAPISession.swift:32-38 — per-call ephemeral session(TCP/TLS 再利用不可、直列呼び出しと相性が悪い)
  • Sources/TrackInteractor/TrackInteractorImpl.swift:182,188-191,200-203 — メタデータ解決 → 歌詞解決が直列、表示candidates.first を使用

対応

原因 1(遅延

  • MetadataRepositoryImpl.resolve() の 3 ソースを async let並列化。
  • MusicBrainzMetadataDataSourceImpl.resolve() の 2 クエリを並列化。
  • fetchLyrics の候補探索を withThrowingTaskGroup で並列化し、最初に validate 成功したものを採用して他をキャンセル(Tier 優先度は保ちたいので、Tier 内で候補を並列 → Tier 間は順序という設計が候補)。
  • タイムアウトの見直し(LRCLIB/MB の 10s は直列合算前提だと重い)。

原因 2(false negative)

  • Tier A にも書き込み前validator.isValid を通す(非対称解消)。または read 時の再バリデーションを「pre-自動・確信度ベースのメタデータ+歌詞解決サイクル #308 判定用のバージョンマーカーを持つ行」だけに限定し、post-自動・確信度ベースのメタデータ+歌詞解決サイクル #308 に書かれた行は書き込み時点で保証済みとして信頼する。
  • LyricsMatchValidator.normalized の付記耐性を上げる((Remastered 2009) / - Live などのサフィックス除去、あるいはトークンベース類似度)。
  • Tier A の書き込み行が validator 基準を満たすことを保証する不変条件テスト追加(現状 LyricsRepositoryTests に該当テストが無く、これが回帰検知できなかった穴)。

関連

  • #308 — 確信度ベースのメタデータ+歌詞解決(本 issue の設計出所
  • #318 — 長時間稼働で歌詞が出なくなる(per-call ephemeral session を導入

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions