Skip to content

Allow manual correction of AI-extracted song metadata #53

@GeneralD

Description

@GeneralD

type complexity estimate AI platform status

記録ステータス: 調査・方針確定済み、実装未着手。大きい機能なので一旦この本文に全調査結果実装計画を保存し、後日ここから着手する。調査時点 = origin/mainc7a0d04(2026-06-14)。作業ブランチ feat/53/manual-metadata-correction(worktree 保持中)。

Motivation

The AI-based title/artist extraction (#51) works well in most cases, but occasionally returns incorrect results. Once a wrong result is cached, it persists until the cache is manually cleared. Users need a way to correct misidentified songs without developer intervention.

Proposal

Provide a mechanism for users to override AI-extracted metadata when it is incorrect. Possible approaches:

  • Keyboard shortcut or menu to cycle through alternative candidates
  • A way to clear the cached AI result for the current song and retry
  • Manual title/artist override via UI or config

Related


確定スコープ

  • 1(候補サイクル UI)+ 案2(キャッシュクリア再試行)+ 案3(手動オーバーライド)すべてを実装する。
  • 手動オーバーライドの永続化はキャッシュ直書き方式。 ただし後述の is_override フラグ列で保護する。
  • 1 UINSStatusItem(メニューバー常駐)実現する(グローバルホットキーは採らない)。

調査結果

A. メタデータ解決パイプライン

解決の優先順位(Sources/MetadataRepository/MetadataRepositoryImpl.swift):

  1. LLM cache(ai_metadata_cache
  2. LLM DataSource(LLMMetadataDataSourceImpl
  3. MusicBrainz cache(musicbrainz_cache
  4. MusicBrainz DataSource
  5. Regex DataSource(キャッシュなし)

要点:

  • LLM は常に単一候補しか返さない([Track] だが要素は最大1)。複数候補を返すのは MusicBrainz(最大5件 × 各3バリエーション)と Regex。
  • 候補配列は呼び出し元まで渡るが、キャッシュへの write は常に .first のみ
  • MetadataUseCaseImplSources/MetadataUseCase/MetadataUseCaseImpl.swift)には resolve()(先頭のみ)と resolveCandidates()(全候補) の両方が公開済み。→ 候補サイクル UI はこの resolveCandidates() をそのまま使える。
  • LLM 抽出(Sources/MetadataDataSource/LLMMetadataDataSourceImpl.swift)は configDataSource.load()?.config.ai から AIEndpoint を取り、MetadataExtractionPrompt を組んで OpenAICompatible(Papyrus)で chat completion を叩き、{title, artist} にデコードする。入力は MediaRemote の生 track.title / track.artist 文字列。

B. キャッシュとストア

  • プロトコル MetadataDataStoreSources/Domain/DataStore/MetadataDataStore.swift)は read(title:artist:)write(title:artist:value:) のみdelete / clear存在しない。
  • writeonConflict: .replace(UPSERT 相当)なので上書きは可能
  • キャッシュキーは両ストアとも (raw_title, raw_artist) の複合ユニークキー。すなわち MediaRemote が報告する生の曲名・アーティスト文字列のペア。ファイルパスや内部 ID は使っていない。
  • テーブル定義(Sources/SQLiteDataStore/DatabaseManager.swift のマイグレーション):
    • v3 ai_metadata_cache(raw_title, raw_artist)(resolved_title, resolved_artist)GRDBLLMMetadataDataStore
    • v2 musicbrainz_cache(query_title, query_artist)(resolved_title, resolved_artist, duration, musicbrainz_id)GRDBMetadataDataStore
  • SQLite 実体は ~/.cache/lyra/database 単一ファイル(XDG_CACHE_HOME 尊重。Folder+DefaultCache.swift / DatabaseManager.swift)。歌詞・Wallpaper キャッシュも同居

C. オーバーレイの入力ハンドリング

  • AppWindowSources/Views/Overlay/AppWindow.swift)は styleMask: .borderlessignoresMouseEvents = true + window level = desktopWindow + 1(通常ウィンドウより下)+ collectionBehavior = [.canJoinAllSpaces, .stationary, .ignoresCycle]canBecomeKey/canBecomeMain は未オーバーライド(→ false)。
  • アプリの activationPolicy は .accessorySources/App/ForegroundApplication.swift)。
  • オーバーレイウィンドウ自体はキー入力もクリックも受け取れない(クリックは下のウィンドウへ透過する意図設計)。
  • グローバルモニタは RipplePresenterSources/Presenters/Wallpaper/RipplePresenter.swift)の NSEvent.addGlobalMonitorForEvents(matching: .mouseMoved) が唯一。キーボードモニタは未使用。
  • 重要訂正: 初期調査で「.accessory だと NSStatusItem を持てない」と報告したが、これは誤り.accessory(LSUIElement 相当)アプリでもメニューバー常駐アイコン NSStatusItem は所有できる(メニューバーに独自のアプリメニューが出ないだけで、ステータスバー項目は別物)。これが案1 UI実現手段になる。

D. CLI / config / daemon

  • 既存サブコマンド(Sources/CLI/Commands/RootCommand.swift): start / stop / restart / service / completion / version / daemon / healthcheck / config / track / benchmarkcache 操作系コマンドは無い。
  • lyra track -rTrackCommand / TrackHandlerImpl)がワンショットでメタデータ解決を走らせる経路。
  • AIConfigSources/Entity/Config/AIConfig.swift)は endpoint / model / apiKey のみ。override 用フィールド無し。
  • daemon と CLI は別プロセスで IPC が一切無い(XPC / Darwin notify / Unix socket / NSDistributedNotification すべて未実装)。両者は SQLite キャッシュ経由でのみ間接通信する。
  • daemon 側 TrackInteractorImplSources/TrackInteractor/TrackInteractorImpl.swift)は observeNowPlaying() を購読し removeDuplicates(by: sameTrack)同一トラックのイベントを無視する。→ CLI でキャッシュを書き換えても「今まさに再生中の曲」は自動再描画されない(次曲 / 同曲再生し直し / lyra restart必要)。

方針の根拠 — なぜ NSStatusItem

NSStatusItem のメニューは daemon プロセス内で動く。したがって Presenter / Interactor に直接アクセスでき、ユーザーが候補を選んだ瞬間に新しい TrackUpdateTrackInteractor から直接流して再描画できる(removeDuplicates(by: sameTrack) を迂回する経路を1本足すだけ)。

つまり CLI 経由では IPC が無くて不可能だった「再生中の曲への即時反映」が、UI(in-process)経由なら IPC 無しで実現する。 これが案1 UI を採る最大の利点であり、グローバルホットキー.keyDown グローバルモニタ / CGEventTap は Accessibility 許可必要で侵襲的)を避ける理由


メニュー構成案(案1 UI

[lyra アイコン]
├─ 現在: <resolved title> — <resolved artist>   (表示のみ・無効化)
├─ 候補を選ぶ ▸
│    ├─ ✓ <candidate 1>
│    ├─   <candidate 2>
│    └─   <candidate 3> …          (MetadataUseCase.resolveCandidates の結果)
├─ 手動で入力…                      (title/artist 入力パネル)
├─ ─────────────
└─ キャッシュをクリアして再解決      (案2)
  • 「候補を選ぶ」「手動で入力」= 案3(オーバーライドをキャッシュへ直書き + 即時反映
  • 「キャッシュをクリアして再解決」= 案2

実装計画(.claude/rules/module-checklist.md 準拠

  1. キャッシュ削除 API(案2MetadataDataStore プロトコルに delete(title:artist:)追加し、GRDBLLMMetadataDataStore / GRDBMetadataDataStore実装
  2. オーバーライド保護フラグai_metadata_cacheis_override INTEGER DEFAULT 0 列をマイグレーション追加。手動値は is_override = 1 で write し、案2のクリアは is_override = 0 の行だけ削除する。これで「キャッシュ直書き」のまま、再解決クリアがユーザーの手動修正を巻き込まない(直書き方式の弱点フラグ1本で補う)。
  3. 候補取得既存 MetadataUseCase.resolveCandidates(track:) を流用。
  4. メニューバー UI(案1 — Views/App 層に NSStatusItem 所有者を新設(VIPER に合わせ Presenter + Interactor 連携AppRouter で wireframe 配線)。
  5. 即時反映 — オーバーライド適用後に TrackInteractor が現在トラックを再 emit する経路を追加removeDuplicates 迂回)。
  6. CLI(補助)同じ Handler を共有して lyra metadata clear / lyra metadata set --title --artist提供UI を使わない運用・スクリプト向け。CLI 経由は従来どおり次曲 or lyra restart反映)。

論点実装着手時に詰める)

  1. 手動入力パネルの activation.accessory のままテキスト入力にフォーカスを当てられるか。必要なら入力中だけ一時的に .regular へ昇格し、閉じたら戻す。
  2. 候補の鮮度 — メニューを開いた時点で resolveCandidates を都度叩くか、直近結果をキャッシュするか。
  3. CLI のコマンド形metadata を独立サブコマンドにするか trackフラグ拡張にするか。
  4. MusicBrainz 側のオーバーライドis_override フラグai_metadata_cache のみ。MusicBrainz cache 由来の誤りも上書き対象にするか(解決順で LLM cache が先に当たるため、LLM 側に override を書けば実質カバーできる見込み)。

着手の起点(再開時メモ)

  • ブランチ: feat/53/manual-metadata-correction(worktree .claude/worktrees/feat-53-metadata-correctionorigin/main 起点)
  • まず手を付けるべきは 手順1delete 追加)→ 手順2is_override マイグレーション)永続層から。UI手順4)はその後
  • 主要ファイル: Sources/Domain/DataStore/MetadataDataStore.swift / Sources/SQLiteDataStore/{GRDBLLMMetadataDataStore,GRDBMetadataDataStore,DatabaseManager}.swift / Sources/MetadataRepository/MetadataRepositoryImpl.swift / Sources/MetadataUseCase/MetadataUseCaseImpl.swift / Sources/TrackInteractor/TrackInteractorImpl.swift / Sources/App/ForegroundApplication.swift / Sources/AppRouter/(NSStatusItem 配線先)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions