// lib/metadata/metadata_stability.dart import 'metadata_model.dart'; /// 状态机判定结果 class StabilityDecision { /// 'verified' | 'pending' final String status; final String reason; final int validationCount; const StabilityDecision({ required this.status, required this.reason, required this.validationCount, }); bool get isVerified => status == 'verified'; } /// 元数据状态机:决定一条记录能不能升级为 verified。 /// /// 第 2 步的规则是"置信度 >= 0.6 且验证次数 >= 2",太粗暴: /// 两次读到的是不同结果也会被当成"验证通过"。这里加入一致性检查—— /// /// - 首次见到 → pending(需要第二次确认) /// - 与上次记录冲突(标题或艺术家明显不同)→ 次数重置为 1,继续 pending /// - 一致且次数 >= 2、置信度达标 → verified /// - 置信度不足 → pending /// /// 这样一次误读不会把错误标签固化,下次播放会重新校正。 class MetadataStability { static const double threshold = 0.6; static StabilityDecision decide({ required Map? previous, required CandidateMetadata candidate, required String title, required String artist, }) { if (previous == null) { return const StabilityDecision( status: 'pending', reason: 'first_seen', validationCount: 1, ); } final prevConfidence = (previous['confidence'] as num?)?.toDouble() ?? 0.0; final prevTitle = previous['title'] as String? ?? ''; final prevArtist = previous['artist'] as String? ?? ''; final prevCount = (previous['validation_count'] as int?) ?? 0; final consistent = _similar(prevTitle, title) && _similar(prevArtist, artist); if (!consistent) { return const StabilityDecision( status: 'pending', reason: 'conflict', validationCount: 1, ); } final count = prevCount + 1; final best = candidate.confidence > prevConfidence ? candidate.confidence : prevConfidence; if (best < threshold) { return StabilityDecision( status: 'pending', reason: 'low_confidence', validationCount: count, ); } if (count < 2) { return StabilityDecision( status: 'pending', reason: 'need_second_validation', validationCount: count, ); } return StabilityDecision( status: 'verified', reason: 'stable', validationCount: count, ); } /// 字段缺失不算冲突;只有两边都有值且不同才算。 static bool _similar(String a, String b) { final left = a.trim().toLowerCase(); final right = b.trim().toLowerCase(); if (left.isEmpty || right.isEmpty) return true; return left == right; } }