Files
lxh2875931338 b8138ad125 feat(metadata): 元数据引擎第 3 步之一——一致性与冲突感知的状态机
之前只要 confidence>=0.6 且 validation_count>=2 就升级 verified,
两次读到不同结果也会被当成"验证通过",错误标签一旦写入很难纠正。

新增 MetadataStability(纯函数,可单测):
- 首次见到 → pending(first_seen),次数 1
- 与上次记录标题/艺术家冲突 → pending(conflict),次数重置为 1
- 一致且次数 >= 2、置信度达标 → verified(stable)
- 置信度不足 → pending(low_confidence),次数继续累计
- 字段缺失不算冲突;比较时忽略大小写与首尾空格

metadata_service 改为调用该状态机,并打印判定原因:
  🧭 [MetadataService] 状态机: stable → verified(validations=2)

测试:新增 test/metadata_stability_test.dart 7 个用例,
连同原有 9 个共 16 个,flutter test 全绿。
已构建并安装 release 验证。
2026-10-24 20:24:55 +08:00

99 lines
2.8 KiB
Dart

// 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<String, dynamic>? 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;
}
}