     ## <img src="https://mojiemoji.jozo.beer/emoji/%E5%95%8F%E9%A1%8C?font=chikara&color=10b981&animation=kage_kaiten&speed=slow&background=transparent&outline=8110b9&outline_width=2" alt="問題" height="20" align="absmiddle"> `#308`(確信度ベースのメタデータ+歌詞解決)以降、実運用で 2 つの体感<img src="https://mojiemoji.jozo.beer/emoji/%E5%8A%A3%E5%8C%96?font=gothic-bold&color=f87171&animation=bane&speed=slow&background=transparent&outline=71f871&outline_width=2" alt="劣化" height="24" align="absmiddle">が起きている。両者は独立の症状に見えるが、**どちらも「全候補 × 全ソース × 全 Tier を直列で試す」という #308 の<img src="https://mojiemoji.jozo.beer/emoji/%E8%A8%AD%E8%A8%88?font=maru&color=4ade80&animation=tatemoya&background=transparent&outline=4a16a3&outline_width=2" alt="設計" height="20" align="absmiddle">と `#318` の per-call ephemeral session が噛み合った<img src="https://mojiemoji.jozo.beer/emoji/%E5%89%AF?font=zero&color=60a5fa&animation=kage_bokashi&background=transparent&outline=fa60a5&outline_width=2" alt="副" height="20" align="absmiddle"><img src="https://mojiemoji.jozo.beer/emoji/%E4%BD%9C%E7%94%A8?font=zero&color=60a5fa&animation=kage_bokashi&background=transparent&outline=fa60a5&outline_width=2" alt="作用" height="20" align="absmiddle">**で、相互に<img src="https://mojiemoji.jozo.beer/emoji/%E6%82%AA%E5%8C%96?font=maru-bold&color=fb923c&animation=yurayura&speed=slow&background=transparent&outline=3cfb92&outline_width=2" alt="悪化" height="24" align="absmiddle">させ合っている。 ### 症状 A — 未キャッシュ曲の歌詞<img src="https://mojiemoji.jozo.beer/emoji/%E8%A1%A8%E7%A4%BA?font=gothic&color=a855f7&animation=bure&background=transparent&outline=f7a855&outline_width=2" alt="表示" height="20" align="absmiddle">が極端に遅い 以前よりも、キャッシュされていない曲の<img src="https://mojiemoji.jozo.beer/emoji/%E6%AD%8C%E8%A9%9E?font=mincho&color=fbbf24&animation=poyoon&speed=slow&background=transparent&outline=24fbbf&outline_width=2" alt="歌詞" height="24" align="absmiddle">が出るまでがずっと遅くなった。曲の中盤くらいになってやっと<img src="https://mojiemoji.jozo.beer/emoji/%E8%A1%A8%E7%A4%BA?font=dela&color=4ade80&animation=nami&speed=slow&background=transparent&outline=804ade&outline_width=2" alt="表示" height="24" align="absmiddle">されることもある。 ### 症状 B — 有名曲を間欠的に取りこぼす タイトル/アーティストはカバー曲でも原曲のものがきちんと取れていて**一見<img src="https://mojiemoji.jozo.beer/emoji/%E7%B2%BE%E5%BA%A6?font=gothic-bold&color=22d3ee&animation=gatagata&speed=slow&background=transparent&outline=ee22d3&outline_width=2" alt="精度" height="24" align="absmiddle">が高そうに見える**のに、<img src="https://mojiemoji.jozo.beer/emoji/%E6%9C%AC%E6%9D%A5?font=akzk&color=c084fc&animation=yurayura&background=transparent&outline=fcc084&outline_width=2" alt="本来" height="20" align="absmiddle">見つからないはずのない<img src="https://mojiemoji.jozo.beer/emoji/%E6%9C%89%E5%90%8D?font=maru-bold&color=60a5fa&animation=bure&speed=slow&background=transparent&outline=fa60a5&outline_width=2" alt="有名" height="24" align="absmiddle">曲の歌詞を間欠的に取りこぼすようになった。 ## 原因<img src="https://mojiemoji.jozo.beer/emoji/%E5%88%86%E6%9E%90?font=rampart&color=f87171&animation=kirari&background=transparent&outline=26dc26&outline_width=2" alt="分析" height="20" align="absmiddle"> ### 原因 1(症状 A)— メタデータ・歌詞解決の全経路が直列全探索 `#308` が「LLM が当たっても短絡せず全ソースを試す/全候補を全 Tier で試す」<img src="https://mojiemoji.jozo.beer/emoji/%E6%96%B9%E9%87%9D?font=mincho&color=818cf8&animation=tenmetsu&speed=slow&background=transparent&outline=f8818c&outline_width=2" alt="方針" height="24" align="absmiddle">に倒したため、未キャッシュ曲では以下がすべて **直列 `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 未<img src="https://mojiemoji.jozo.beer/emoji/%E8%A8%AD%E5%AE%9A?font=rampart&color=60a5fa&animation=mabataki&background=transparent&outline=fa60a5&outline_width=2" alt="設定" height="20" align="absmiddle">)** | | **約 280 秒(≈4 分 40 秒)** | `async let` / `withThrowingTaskGroup` は使われておらず、`MetadataRepositoryImpl.resolve()` の 3 ソースも、`fetchLyrics` の候補ループもすべて逐次 `await`。候補数は区切り記号の多い<img src="https://mojiemoji.jozo.beer/emoji/%E6%9B%B2%E5%90%8D?font=dela&color=c084fc&animation=shuchusen&speed=slow&background=transparent&outline=fcc084&outline_width=2" alt="曲名" height="24" align="absmiddle">や MusicBrainz の複数レコーディングで 5〜15 件になり得るため、`10s × N × 2 Tier` がそのまま効く。「曲の中盤でやっと<img src="https://mojiemoji.jozo.beer/emoji/%E8%A1%A8%E7%A4%BA?font=gothic&color=a855f7&animation=bure&background=transparent&outline=f7a855&outline_width=2" alt="表示" height="20" align="absmiddle">」という体感(一般的なポップスで 3〜5 分)はこの見積もりレンジと<img src="https://mojiemoji.jozo.beer/emoji/%E6%95%B4%E5%90%88?font=gothic-bold&color=f472b6&animation=mochimochi&speed=slow&background=transparent&outline=b6f472&outline_width=2" alt="整合" height="24" align="absmiddle">する。 ### 原因 2(症状 B)— Tier A だけがバリデーション無しでキャッシュ書き込みする非<img src="https://mojiemoji.jozo.beer/emoji/%E5%AF%BE%E7%A7%B0?font=dela&color=ef4444&animation=mabataki&background=transparent&outline=44ef44&outline_width=2" alt="対称" height="20" align="absmiddle"> `fetchLyrics(candidates:)` は冒頭のキャッシュ read で **全行を無<img src="https://mojiemoji.jozo.beer/emoji/%E6%9D%A1%E4%BB%B6?font=maru-bold&color=34d399&animation=norinori&speed=slow&background=transparent&outline=9934d3&outline_width=2" alt="条件" height="24" align="absmiddle">で再バリデート**する(`#308` が pre-#308 の poisoned row を握りつぶすために入れたもの)。ところが書き込み側は非<img src="https://mojiemoji.jozo.beer/emoji/%E5%AF%BE%E7%A7%B0?font=mincho&color=f87171&animation=yoko_scroll&speed=slow&background=transparent&outline=71f871&outline_width=2" alt="対称" height="24" align="absmiddle">で、**Tier A(`tierAExactMatch`)だけが `LyricsMatchValidator` を通さずに `store()` する**(Tier B/C は書き込み<img src="https://mojiemoji.jozo.beer/emoji/%E5%89%8D?font=maru&color=8b5cf6&animation=yokomoya&background=transparent&outline=f68b5c&outline_width=2" alt="前" height="20" align="absmiddle">に必ず `isValid` を通す)。 さらに `LyricsMatchValidator.normalized` は `text.lowercased().filter { $0.isLetter || $0.isNumber }` で**空白・記号を全<img src="https://mojiemoji.jozo.beer/emoji/%E9%99%A4%E5%8E%BB?font=dela&color=fb923c&animation=tate_scroll&speed=slow&background=transparent&outline=3cfb92&outline_width=2" alt="除去" height="24" align="absmiddle">して<img src="https://mojiemoji.jozo.beer/emoji/%E9%80%A3%E7%B5%90?font=kurobara&color=22c55e&animation=norinori&background=transparent&outline=5e22c5&outline_width=2" alt="連結" height="20" align="absmiddle">**する方式のため、括弧書き・付記を特別扱いしない。短いタイトルに定型サフィックスが付くと簡単に<img src="https://mojiemoji.jozo.beer/emoji/%E9%96%BE%E5%80%A4?font=gothic-bold&color=fbbf24&animation=ekken&speed=slow&background=transparent&outline=24fbbf&outline_width=2" alt="閾値" height="24" align="absmiddle"> `0.6` を割る。 ```text candidate "Yesterday" → "yesterday" (9) LRCLIB "Yesterday - Remastered 2009" → "yesterdayremastered2009" (24) Levenshtein=15 類似度 = 1 - 15/24 = 0.375 → 0.6 未満で invalid ``` 有名曲ほどリマスター/ライブ/別バージョン<img src="https://mojiemoji.jozo.beer/emoji/%E8%A1%A8%E8%A8%98?font=maru-bold&color=4ade80&animation=tate_ekken&speed=slow&background=transparent&outline=804ade&outline_width=2" alt="表記" height="24" align="absmiddle">のカタ<img src="https://mojiemoji.jozo.beer/emoji/%E3%83%AD%E3%82%B0?font=maru-bold&color=10b981&animation=tatemoya&background=transparent&outline=8110b9&outline_width=2" alt="ログ" height="20" align="absmiddle">を持つため、**無名曲より有名曲の方が引っかかりやすい非<img src="https://mojiemoji.jozo.beer/emoji/%E5%AF%BE%E7%A7%B0?font=mincho&color=10b981&animation=zairu&background=transparent&outline=8110b9&outline_width=2" alt="対称" height="20" align="absmiddle">性**を持つ。 **false negative のループ:** 1. 有名曲を初回再生 → Tier A が LRCLIB `/api/get` にヒットし、**無<img src="https://mojiemoji.jozo.beer/emoji/%E6%A4%9C%E8%A8%BC?font=mincho&color=22d3ee&animation=neruneru&speed=slow&background=transparent&outline=ee22d3&outline_width=2" alt="検証" height="24" align="absmiddle">のまま**閾値割れの行をキャッシュ書き込み。歌詞は直接 return されるので初回は正しく<img src="https://mojiemoji.jozo.beer/emoji/%E8%A1%A8%E7%A4%BA?font=gothic&color=a855f7&animation=bure&background=transparent&outline=f7a855&outline_width=2" alt="表示" height="20" align="absmiddle">される(title/artist も原曲寄りで「精度が高そうに見える」)。 2. 2 回目以降の再生 → read ループで<img src="https://mojiemoji.jozo.beer/emoji/%E5%90%8C%E3%81%98?font=hachimaru&color=f472b6&animation=yurayura&background=transparent&outline=b6f472&outline_width=2" alt="同じ" height="20" align="absmiddle">行がヒットするが `validator.isValid` が **false** → `continue` でキャッシュを握りつぶし、<img src="https://mojiemoji.jozo.beer/emoji/%E7%9B%B4%E5%88%97?font=dela&color=60a5fa&animation=patapata&speed=slow&background=transparent&outline=fa60a5&outline_width=2" alt="直列" height="24" align="absmiddle">ライブ再取得(原因 1)に落ちる。 3. ライブ再取得は毎回 10s タイムアウトのネットワーク<img src="https://mojiemoji.jozo.beer/emoji/%E5%BE%80%E5%BE%A9?font=gothic-bold&color=818cf8&animation=mabataki&speed=slow&background=transparent&outline=f8818c&outline_width=2" alt="往復" height="24" align="absmiddle">。LRCLIB の<img src="https://mojiemoji.jozo.beer/emoji/%E4%B8%80%E6%99%82?font=gothic&color=fbbf24&animation=disco&background=transparent&outline_width=0" alt="一時" height="20" align="absmiddle">的な 5xx / タイムアウト / レート<img src="https://mojiemoji.jozo.beer/emoji/%E5%88%B6%E9%99%90?font=maru-bold&color=eab308&animation=kage_kaiten&speed=slow&background=transparent&outline=08eab3&outline_width=2" alt="制限" height="20" align="absmiddle">に晒される。Tier C は `fallback_command` 未<img src="https://mojiemoji.jozo.beer/emoji/%E8%A8%AD%E5%AE%9A?font=maru-bold&color=f472b6&animation=mochimochi&background=transparent&outline=b6f472&outline_width=2" alt="設定" height="20" align="absmiddle">なら即 `nil` で<img src="https://mojiemoji.jozo.beer/emoji/%E5%AE%89%E5%85%A8?font=maru-bold&color=c084fc&animation=tatemoya&speed=slow&background=transparent&outline=fcc084&outline_width=2" alt="安全" height="24" align="absmiddle">網にならない。 4. 全 Tier が `nil` → `fetchLyrics` が `nil` → <img src="https://mojiemoji.jozo.beer/emoji/UI?font=kurobara&color=c084fc&animation=ekken&background=transparent&outline=fcc084&outline_width=2" alt="UI" height="20" align="absmiddle"> は `.notFound`(歌詞なし)。 つまり症状 B の実害は、**原因 1 の「遅い直列全探索」があるからこそ、キャッシュを握りつぶすたびにネットワーク不調に晒されて<img src="https://mojiemoji.jozo.beer/emoji/%E9%A1%95%E5%9C%A8?font=mincho&color=f472b6&animation=yokomoya&speed=slow&background=transparent&outline=b6f472&outline_width=2" alt="顕在" height="24" align="absmiddle">化**する。両者は<img src="https://mojiemoji.jozo.beer/emoji/%E5%90%8C%E3%81%98?font=hachimaru&color=f472b6&animation=yurayura&background=transparent&outline=b6f472&outline_width=2" alt="同じ" height="20" align="absmiddle"> #308 <img src="https://mojiemoji.jozo.beer/emoji/%E8%A8%AD%E8%A8%88?font=gothic-bold&color=06b6d4&animation=kirari&background=transparent&outline=d406b6&outline_width=2" alt="設計" height="20" align="absmiddle">から派生し、相互に<img src="https://mojiemoji.jozo.beer/emoji/%E5%A2%97%E5%B9%85?font=dela&color=34d399&animation=zairu&speed=slow&background=transparent&outline=9934d3&outline_width=2" alt="増幅" height="24" align="absmiddle">している。 ### <img src="https://mojiemoji.jozo.beer/emoji/%E8%A3%9C%E8%B6%B3?font=mincho&color=06b6d4&animation=disco&background=transparent&outline_width=0" alt="補足" height="20" align="absmiddle"> — <img src="https://mojiemoji.jozo.beer/emoji/%E8%A1%A8%E7%A4%BA?font=gothic&color=a855f7&animation=bure&background=transparent&outline=f7a855&outline_width=2" alt="表示" height="20" align="absmiddle">と歌詞検索の経路<img src="https://mojiemoji.jozo.beer/emoji/%E5%88%86%E9%9B%A2?font=gothic&color=f87171&animation=chirichiri&background=transparent&outline=26dc26&outline_width=2" alt="分離" height="20" align="absmiddle"> <img src="https://mojiemoji.jozo.beer/emoji/%E8%A1%A8%E7%A4%BA?font=dela&color=22c55e&animation=gatagata&background=transparent&outline=5e22c5&outline_width=2" alt="表示" height="20" align="absmiddle">用タイトル/アーティストは常に `candidates.first`(=LLM 補正済みの<img src="https://mojiemoji.jozo.beer/emoji/%E7%B6%BA%E9%BA%97?font=rampart&color=f472b6&animation=ekken&background=transparent&outline=b6f472&outline_width=2" alt="綺麗" height="20" align="absmiddle">な名前)を使う一方、歌詞取得はフル候補配列を別<img src="https://mojiemoji.jozo.beer/emoji/%E7%B5%8C%E8%B7%AF?font=gothic-bold&color=f87171&animation=zanzo&speed=slow&background=transparent&outline=71f871&outline_width=2" alt="経路" height="24" align="absmiddle">で処理する。これ自体は false negative の直接原因ではないが、「<img src="https://mojiemoji.jozo.beer/emoji/%E8%A1%A8%E7%A4%BA?font=gothic&color=a855f7&animation=bure&background=transparent&outline=f7a855&outline_width=2" alt="表示" height="20" align="absmiddle">は<img src="https://mojiemoji.jozo.beer/emoji/%E7%B6%BA%E9%BA%97?font=hachimaru&color=8b5cf6&animation=mochimochi&background=transparent&outline=f68b5c&outline_width=2" alt="綺麗" height="20" align="absmiddle">なのに歌詞が出ない」という体感の<img src="https://mojiemoji.jozo.beer/emoji/%E3%82%BA%E3%83%AC?font=maru-bold&color=fb923c&animation=chirichiri&speed=slow&background=transparent&outline=3cfb92&outline_width=2" alt="ズレ" height="24" align="absmiddle">を作っている。 ## <img src="https://mojiemoji.jozo.beer/emoji/%E5%86%8D%E7%8F%BE?font=maru&color=eab308&animation=ekken&background=transparent&outline=08eab3&outline_width=2" alt="再現" height="20" align="absmiddle"><img src="https://mojiemoji.jozo.beer/emoji/%E6%89%8B%E9%A0%86?font=kurobara&color=c084fc&animation=kaiten&speed=slow&background=transparent&outline=fcc084&outline_width=2" alt="手順" height="20" align="absmiddle"> 1. LLM(`[ai]`)を<img src="https://mojiemoji.jozo.beer/emoji/%E8%A8%AD%E5%AE%9A?font=rampart&color=60a5fa&animation=mabataki&background=transparent&outline=fa60a5&outline_width=2" alt="設定" height="20" align="absmiddle">した状態で lyra daemon を<img src="https://mojiemoji.jozo.beer/emoji/%E8%B5%B7%E5%8B%95?font=mincho&color=fbbf24&animation=kirari&speed=slow&background=transparent&outline=24fbbf&outline_width=2" alt="起動" height="24" align="absmiddle">。 2. キャッシュに無い曲(特にリマスター/ライブ表記のある有名曲)を<img src="https://mojiemoji.jozo.beer/emoji/%E5%86%8D%E7%94%9F?font=dela&color=4ade80&animation=yatta&speed=slow&background=transparent&outline=804ade&outline_width=2" alt="再生" height="24" align="absmiddle">する。 3. 症状 A: 歌詞が出るまで数十秒〜数分待たされる(曲の<img src="https://mojiemoji.jozo.beer/emoji/%E4%B8%AD%E7%9B%A4?font=gothic-bold&color=22d3ee&animation=kaiten&speed=slow&background=transparent&outline=ee22d3&outline_width=2" alt="中盤" height="24" align="absmiddle">でやっと<img src="https://mojiemoji.jozo.beer/emoji/%E8%A1%A8%E7%A4%BA?font=dela&color=22c55e&animation=gatagata&background=transparent&outline=5e22c5&outline_width=2" alt="表示" height="20" align="absmiddle">)。 4. <img src="https://mojiemoji.jozo.beer/emoji/%E5%90%8C%E3%81%98?font=hachimaru&color=f472b6&animation=yurayura&background=transparent&outline=b6f472&outline_width=2" alt="同じ" height="20" align="absmiddle">曲を<img src="https://mojiemoji.jozo.beer/emoji/%E5%BE%8C%E6%97%A5?font=maru-bold&color=60a5fa&animation=psycho&speed=slow&background=transparent" alt="後日" height="24" align="absmiddle">もう一度再生する。 5. 症状 B: 初回は出たのに、2 回目以降は<img src="https://mojiemoji.jozo.beer/emoji/%E9%96%93%E6%AC%A0?font=mincho&color=818cf8&animation=disco&speed=slow&background=transparent" alt="間欠" height="24" align="absmiddle">的に「歌詞なし」になる。 ## 期待する<img src="https://mojiemoji.jozo.beer/emoji/%E5%8B%95%E4%BD%9C?font=pixel&color=8b5cf6&animation=poyoon&background=transparent&outline=f68b5c&outline_width=2" alt="動作" height="20" align="absmiddle"> - 未キャッシュ曲でも、候補群を**<img src="https://mojiemoji.jozo.beer/emoji/%E4%B8%A6%E5%88%97?font=dela&color=c084fc&animation=kira&speed=slow&background=transparent" alt="並列" height="24" align="absmiddle">探索**し「最初にヒットしたものを<img src="https://mojiemoji.jozo.beer/emoji/%E6%8E%A1%E7%94%A8?font=gothic-bold&color=f472b6&animation=bane&speed=slow&background=transparent&outline=b6f472&outline_width=2" alt="採用" height="24" align="absmiddle">、他はキャンセル」することで、最悪でも「最も遅い 1 回のタイムアウト」(数秒〜十数秒)で<img src="https://mojiemoji.jozo.beer/emoji/%E8%A1%A8%E7%A4%BA?font=kurobara&color=fdba74&animation=kaiten&speed=slow&background=transparent&outline=74fdba&outline_width=2" alt="表示" height="20" align="absmiddle">される。 - 一度キャッシュに乗った有名曲は、read 時の再バリデーションで握りつぶされず、再生のたびに<img src="https://mojiemoji.jozo.beer/emoji/%E5%AE%89%E5%AE%9A?font=maru-bold&color=34d399&animation=yurayura&speed=slow&background=transparent&outline=9934d3&outline_width=2" alt="安定" height="24" align="absmiddle">して<img src="https://mojiemoji.jozo.beer/emoji/%E8%A1%A8%E7%A4%BA?font=kurobara&color=fdba74&animation=kaiten&speed=slow&background=transparent&outline=74fdba&outline_width=2" alt="表示" height="20" align="absmiddle">される。 ## 実際の<img src="https://mojiemoji.jozo.beer/emoji/%E5%8B%95%E4%BD%9C?font=pixel&color=8b5cf6&animation=poyoon&background=transparent&outline=f68b5c&outline_width=2" alt="動作" height="20" align="absmiddle"> - 未キャッシュ曲は直列全<img src="https://mojiemoji.jozo.beer/emoji/%E6%8E%A2%E7%B4%A2?font=mincho&color=f87171&animation=poyoon&speed=slow&background=transparent&outline=71f871&outline_width=2" alt="探索" height="24" align="absmiddle">で最悪 4〜5 分待たされる。 - Tier A が閾値割れの行を書くと、2 回目以降の再生で毎回キャッシュが握りつぶされ、ネットワーク<img src="https://mojiemoji.jozo.beer/emoji/%E4%B8%8D%E8%AA%BF?font=dela&color=fb923c&animation=nami&speed=slow&background=transparent&outline=3cfb92&outline_width=2" alt="不調" height="24" align="absmiddle">のたびに歌詞なしへ<img src="https://mojiemoji.jozo.beer/emoji/%E9%80%80%E8%A1%8C?font=gothic-bold&color=fbbf24&animation=gatagata&speed=slow&background=transparent&outline=24fbbf&outline_width=2" alt="退行" height="24" align="absmiddle">する。 ## 該当箇所 - `Sources/MetadataRepository/MetadataRepositoryImpl.swift:15-22` — LLM → MusicBrainz → Regex を `async let`/TaskGroup 無しで<img src="https://mojiemoji.jozo.beer/emoji/%E7%9B%B4%E5%88%97?font=mincho&color=f87171&animation=kirari&speed=slow&background=transparent&outline=71f871&outline_width=2" alt="直列" height="24" align="absmiddle"> `await` - `Sources/MetadataDataSource/MusicBrainzMetadataDataSourceImpl.swift:35-45` — 2 クエリを直列ループ、両方<img src="https://mojiemoji.jozo.beer/emoji/%E5%A4%B1%E6%95%97?font=maru-bold&color=4ade80&animation=bure&speed=slow&background=transparent&outline=804ade&outline_width=2" alt="失敗" height="24" align="absmiddle">で 20s - `Sources/LyricsRepository/LyricsRepositoryImpl.swift:39-47` — キャッシュ read が全<img src="https://mojiemoji.jozo.beer/emoji/%E5%80%99%E8%A3%9C?font=mincho&color=22d3ee&animation=tenmetsu&speed=slow&background=transparent&outline=ee22d3&outline_width=2" alt="候補" height="24" align="absmiddle">直列 + **全行を無条件で再バリデート** - `Sources/LyricsRepository/LyricsRepositoryImpl.swift:64-74` — Tier A が `validator.isValid` を**通さず** `store()`(Tier B: 88 行目 / Tier C: 106 行目は通している) - `Sources/LyricsRepository/LyricsMatchValidator.swift:28-30` — `normalized` が空白・記号を全<img src="https://mojiemoji.jozo.beer/emoji/%E9%99%A4%E5%8E%BB?font=chikara&color=60a5fa&animation=yatta&background=transparent&outline=eb2563&outline_width=2" alt="除去" height="20" align="absmiddle">して<img src="https://mojiemoji.jozo.beer/emoji/%E9%80%A3%E7%B5%90?font=dela&color=60a5fa&animation=shuchusen&speed=slow&background=transparent&outline=fa60a5&outline_width=2" alt="連結" height="24" align="absmiddle">(付記に弱い)/閾値 `0.6`(4 行目) - `Sources/ScopedAPISession/ScopedAPISession.swift:32-38` — per-call ephemeral session(TCP/TLS 再利用不可、直列呼び出しと<img src="https://mojiemoji.jozo.beer/emoji/%E7%9B%B8%E6%80%A7?font=gothic-bold&color=818cf8&animation=mochimochi&speed=slow&background=transparent&outline=f8818c&outline_width=2" alt="相性" height="24" align="absmiddle">が悪い) - `Sources/TrackInteractor/TrackInteractorImpl.swift:182,188-191,200-203` — メタデータ<img src="https://mojiemoji.jozo.beer/emoji/%E8%A7%A3%E6%B1%BA?font=maru-bold&color=c084fc&animation=norinori&speed=slow&background=transparent&outline=fcc084&outline_width=2" alt="解決" height="24" align="absmiddle"> → 歌詞解決が直列、<img src="https://mojiemoji.jozo.beer/emoji/%E8%A1%A8%E7%A4%BA?font=gothic&color=a855f7&animation=bure&background=transparent&outline=f7a855&outline_width=2" alt="表示" height="20" align="absmiddle">は `candidates.first` を使用 ## <img src="https://mojiemoji.jozo.beer/emoji/%E5%AF%BE%E5%BF%9C?font=pixel&color=fbbf24&animation=tatemoya&background=transparent&outline=06d977&outline_width=2" alt="対応" height="20" align="absmiddle">案 ### 原因 1(<img src="https://mojiemoji.jozo.beer/emoji/%E9%81%85%E5%BB%B6?font=toge&color=f97316&animation=bane&background=transparent&outline=16f973&outline_width=2" alt="遅延" height="20" align="absmiddle">) - `MetadataRepositoryImpl.resolve()` の 3 ソースを `async let` で<img src="https://mojiemoji.jozo.beer/emoji/%E4%B8%A6%E5%88%97?font=dela&color=fb923c&animation=yatta&speed=slow&background=transparent&outline=3cfb92&outline_width=2" alt="並列" height="24" align="absmiddle">化。 - `MusicBrainzMetadataDataSourceImpl.resolve()` の 2 クエリを<img src="https://mojiemoji.jozo.beer/emoji/%E4%B8%A6%E5%88%97?font=gothic-bold&color=fbbf24&animation=kaiten&speed=slow&background=transparent&outline=24fbbf&outline_width=2" alt="並列" height="24" align="absmiddle">化。 - `fetchLyrics` の候補探索を `withThrowingTaskGroup` で並列化し、**最初に validate <img src="https://mojiemoji.jozo.beer/emoji/%E6%88%90%E5%8A%9F?font=gothic-bold&color=facc15&animation=mochimochi&background=transparent&outline=04ca8a&outline_width=2" alt="成功" height="20" align="absmiddle">したものを<img src="https://mojiemoji.jozo.beer/emoji/%E6%8E%A1%E7%94%A8?font=hachimaru&color=f97316&animation=kira&background=transparent&outline_width=0" alt="採用" height="20" align="absmiddle">して他をキャンセル**(Tier <img src="https://mojiemoji.jozo.beer/emoji/%E5%84%AA%E5%85%88?font=mincho&color=f472b6&animation=yoko_scroll&speed=slow&background=transparent&outline=b6f472&outline_width=2" alt="優先" height="24" align="absmiddle">度は保ちたいので、Tier 内で候補を並列 → Tier 間は<img src="https://mojiemoji.jozo.beer/emoji/%E9%A0%86%E5%BA%8F?font=hachimaru&color=eab308&animation=mozaiku&background=transparent&outline=08eab3&outline_width=2" alt="順序" height="20" align="absmiddle">という<img src="https://mojiemoji.jozo.beer/emoji/%E8%A8%AD%E8%A8%88?font=dela&color=34d399&animation=tate_scroll&speed=slow&background=transparent&outline=9934d3&outline_width=2" alt="設計" height="24" align="absmiddle">が候補)。 - タイムアウトの見直し(LRCLIB/MB の 10s は直列<img src="https://mojiemoji.jozo.beer/emoji/%E5%90%88%E7%AE%97?font=gothic-bold&color=f87171&animation=ekken&speed=slow&background=transparent&outline=71f871&outline_width=2" alt="合算" height="24" align="absmiddle">前提だと重い)。 ### 原因 2(false negative) - **Tier A にも書き込み<img src="https://mojiemoji.jozo.beer/emoji/%E5%89%8D?font=chikara&color=ec4899&animation=poyoon&background=transparent&outline=99ec48&outline_width=2" alt="前" height="20" align="absmiddle">に `validator.isValid` を通す**(非<img src="https://mojiemoji.jozo.beer/emoji/%E5%AF%BE%E7%A7%B0?font=dela&color=ef4444&animation=mabataki&background=transparent&outline=44ef44&outline_width=2" alt="対称" height="20" align="absmiddle">の<img src="https://mojiemoji.jozo.beer/emoji/%E8%A7%A3%E6%B6%88?font=maru-bold&color=fb923c&animation=tate_ekken&speed=slow&background=transparent&outline=3cfb92&outline_width=2" alt="解消" height="24" align="absmiddle">)。または read 時の再バリデーションを「pre-#308 判定用のバージョンマーカーを持つ行」だけに限定し、post-#308 に書かれた行は書き込み時点で保証済みとして<img src="https://mojiemoji.jozo.beer/emoji/%E4%BF%A1%E9%A0%BC?font=mincho&color=fbbf24&animation=neruneru&speed=slow&background=transparent&outline=24fbbf&outline_width=2" alt="信頼" height="24" align="absmiddle">する。 - `LyricsMatchValidator.normalized` の付記<img src="https://mojiemoji.jozo.beer/emoji/%E8%80%90%E6%80%A7?font=dela&color=4ade80&animation=patapata&speed=slow&background=transparent&outline=804ade&outline_width=2" alt="耐性" height="24" align="absmiddle">を上げる(`(Remastered 2009)` / `- Live` などのサフィックス<img src="https://mojiemoji.jozo.beer/emoji/%E9%99%A4%E5%8E%BB?font=chikara&color=60a5fa&animation=yatta&background=transparent&outline=eb2563&outline_width=2" alt="除去" height="20" align="absmiddle">、あるいはトークンベース<img src="https://mojiemoji.jozo.beer/emoji/%E9%A1%9E%E4%BC%BC?font=gothic-bold&color=22d3ee&animation=mabataki&speed=slow&background=transparent&outline=ee22d3&outline_width=2" alt="類似" height="24" align="absmiddle">度)。 - Tier A の書き込み行が validator 基準を満たすことを保証する不変条件<img src="https://mojiemoji.jozo.beer/emoji/%E3%83%86%E3%82%B9%E3%83%88?font=maru-bold&color=fbbf24&animation=yokomoya&background=transparent&outline=06d977&outline_width=2" alt="テスト" height="20" align="absmiddle">を<img src="https://mojiemoji.jozo.beer/emoji/%E8%BF%BD%E5%8A%A0?font=maru&color=3b82f6&animation=chirichiri&background=transparent&outline=f63b82&outline_width=2" alt="追加" height="20" align="absmiddle">(現状 `LyricsRepositoryTests` に該当<img src="https://mojiemoji.jozo.beer/emoji/%E3%83%86%E3%82%B9%E3%83%88?font=hachimaru&color=f43f5e&animation=yoko_scroll&background=transparent&outline=5ef43f&outline_width=2" alt="テスト" height="20" align="absmiddle">が無く、これが<img src="https://mojiemoji.jozo.beer/emoji/%E5%9B%9E%E5%B8%B0?font=maru-bold&color=60a5fa&animation=tatemoya&speed=slow&background=transparent&outline=fa60a5&outline_width=2" alt="回帰" height="24" align="absmiddle">を<img src="https://mojiemoji.jozo.beer/emoji/%E6%A4%9C%E7%9F%A5?font=maru&color=eab308&animation=kage_neon&background=transparent&outline=08eab3&outline_width=2" alt="検知" height="20" align="absmiddle">できなかった<img src="https://mojiemoji.jozo.beer/emoji/%E7%A9%B4?font=mincho&color=818cf8&animation=yokomoya&speed=slow&background=transparent&outline=f8818c&outline_width=2" alt="穴" height="24" align="absmiddle">)。 ## <img src="https://mojiemoji.jozo.beer/emoji/%E9%96%A2%E9%80%A3?font=noto&color=f87171&animation=nami&background=transparent&outline=26dc26&outline_width=2" alt="関連" height="20" align="absmiddle"> - `#308` — 確信度ベースのメタデータ+歌詞解決(本 issue の<img src="https://mojiemoji.jozo.beer/emoji/%E8%A8%AD%E8%A8%88?font=gothic-bold&color=06b6d4&animation=kirari&background=transparent&outline=d406b6&outline_width=2" alt="設計" height="20" align="absmiddle">の<img src="https://mojiemoji.jozo.beer/emoji/%E5%87%BA%E6%89%80?font=dela&color=c084fc&animation=zairu&speed=slow&background=transparent&outline=fcc084&outline_width=2" alt="出所" height="24" align="absmiddle">) - `#318` — 長時間<img src="https://mojiemoji.jozo.beer/emoji/%E7%A8%BC%E5%83%8D?font=gothic-bold&color=f472b6&animation=zanzo&speed=slow&background=transparent&outline=b6f472&outline_width=2" alt="稼働" height="24" align="absmiddle">で歌詞が出なくなる(per-call ephemeral session を<img src="https://mojiemoji.jozo.beer/emoji/%E5%B0%8E%E5%85%A5?font=maru-bold&color=34d399&animation=chirichiri&speed=slow&background=transparent&outline=9934d3&outline_width=2" alt="導入" height="24" align="absmiddle">)
#308(確信度ベースのメタデータ+歌詞解決)以降、実運用で 2 つの体感#318の per-call ephemeral session が噛み合った症状 A — 未キャッシュ曲の歌詞
が極端に遅い
以前よりも、キャッシュされていない曲の
が出るまでがずっと遅くなった。曲の中盤くらいになってやっと
されることもある。
症状 B — 有名曲を間欠的に取りこぼす
タイトル/アーティストはカバー曲でも原曲のものがきちんと取れていて一見
が高そうに見えるのに、
見つからないはずのない
曲の歌詞を間欠的に取りこぼすようになった。
原因
原因 1(症状 A)— メタデータ・歌詞解決の全経路が直列全探索
#308が「LLM が当たっても短絡せず全ソースを試す/全候補を全 Tier で試す」awaitで積み上がる。MetadataRepositoryImpl.resolve()LLM#318timeout)fetchLyricsTier A(LRCLIB get)async let/withThrowingTaskGroupは使われておらず、MetadataRepositoryImpl.resolve()の 3 ソースも、fetchLyricsの候補ループもすべて逐次await。候補数は区切り記号の多い10s × N × 2 Tierがそのまま効く。「曲の中盤でやっと原因 2(症状 B)— Tier A だけがバリデーション無しでキャッシュ書き込みする非
fetchLyrics(candidates:)は冒頭のキャッシュ read で 全行を無#308が pre-#308 の poisoned row を握りつぶすために入れたもの)。ところが書き込み側は非tierAExactMatch)だけがLyricsMatchValidatorを通さずにstore()する(Tier B/C は書き込みisValidを通す)。さらに
して
**する方式のため、括弧書き・付記を特別扱いしない。短いタイトルに定型サフィックスが付くと簡単に
LyricsMatchValidator.normalizedはtext.lowercased().filter { $0.isLetter || $0.isNumber }で**空白・記号を全0.6を割る。有名曲ほどリマスター/ライブ/別バージョン
のカタ
を持つため、無名曲より有名曲の方が引っかかりやすい非
性を持つ。
false negative のループ:
/api/getにヒットし、無validator.isValidが false →continueでキャッシュを握りつぶし、fallback_command未nilでnil→fetchLyricsがnil→.notFound(歌詞なし)。つまり症状 B の実害は、原因 1 の「遅い直列全探索」があるからこそ、キャッシュを握りつぶすたびにネットワーク不調に晒されて
化する。両者は
#308
から派生し、相互に
している。
candidates.first(=LLM 補正済みの[ai])を期待する
実際の
該当箇所
Sources/MetadataRepository/MetadataRepositoryImpl.swift:15-22— LLM → MusicBrainz → Regex をasync let/TaskGroup 無しでawaitSources/MetadataDataSource/MusicBrainzMetadataDataSourceImpl.swift:35-45— 2 クエリを直列ループ、両方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-30—normalizedが空白・記号を全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原因 2(false negative)
validator.isValidを通す(非LyricsMatchValidator.normalizedの付記(Remastered 2009)/- LiveなどのサフィックスLyricsRepositoryTestsに該当#308— 確信度ベースのメタデータ+歌詞解決(本 issue の#318— 長時間