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 验证。
This commit is contained in:
@@ -6,6 +6,7 @@ import '../database/song_database.dart';
|
||||
import 'metadata_model.dart';
|
||||
import 'metadata_reader.dart';
|
||||
import 'metadata_normalizer.dart';
|
||||
import 'metadata_stability.dart';
|
||||
import 'metadata_cache.dart';
|
||||
import 'file_provider.dart';
|
||||
import '../utils/artwork_helper.dart';
|
||||
@@ -128,9 +129,15 @@ class MetadataService {
|
||||
final candidate = _normalizer.evaluate(normalized, history);
|
||||
final finalMetadata = _normalizer.decide(candidate);
|
||||
|
||||
// 7. 与已有记录合并
|
||||
// 7. 与已有记录合并(一致性 + 次数由状态机决定)
|
||||
final prevConfidence = (dbSong?['confidence'] as num?)?.toDouble() ?? 0.0;
|
||||
final validationCount = ((dbSong?['validation_count'] as int?) ?? 0) + 1;
|
||||
final decision = MetadataStability.decide(
|
||||
previous: dbSong,
|
||||
candidate: candidate,
|
||||
title: finalMetadata.title,
|
||||
artist: finalMetadata.artist,
|
||||
);
|
||||
final validationCount = decision.validationCount;
|
||||
final useNew = dbSong == null || candidate.confidence >= prevConfidence;
|
||||
final effectiveConfidence =
|
||||
useNew ? candidate.confidence : prevConfidence;
|
||||
@@ -144,8 +151,9 @@ class MetadataService {
|
||||
await ArtworkHelper.saveArtwork(finalMetadata.artwork!, songKey);
|
||||
}
|
||||
|
||||
final status =
|
||||
(effectiveConfidence >= 0.6 && validationCount >= 2) ? 'verified' : 'pending';
|
||||
final status = decision.status;
|
||||
debugPrint('🧭 [MetadataService] 状态机: ${decision.reason} → $status'
|
||||
'(validations=$validationCount)');
|
||||
|
||||
final row = <String, dynamic>{
|
||||
'song_key': songKey,
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user