之前只要 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 验证。
99 lines
2.8 KiB
Dart
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;
|
|
}
|
|
}
|