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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// test/metadata_stability_test.dart
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:qingting_player/metadata/metadata_model.dart';
|
||||
import 'package:qingting_player/metadata/metadata_stability.dart';
|
||||
|
||||
void main() {
|
||||
CandidateMetadata candidate({double confidence = 0.8}) => CandidateMetadata(
|
||||
metadata: const NormalizedMetadata(title: '夜曲', artist: '周杰伦'),
|
||||
confidence: confidence,
|
||||
);
|
||||
|
||||
group('MetadataStability', () {
|
||||
test('首次见到 → pending,次数 1', () {
|
||||
final d = MetadataStability.decide(
|
||||
previous: null,
|
||||
candidate: candidate(),
|
||||
title: '夜曲',
|
||||
artist: '周杰伦',
|
||||
);
|
||||
expect(d.status, 'pending');
|
||||
expect(d.reason, 'first_seen');
|
||||
expect(d.validationCount, 1);
|
||||
});
|
||||
|
||||
test('第二次结果一致且置信度达标 → verified', () {
|
||||
final d = MetadataStability.decide(
|
||||
previous: {
|
||||
'title': '夜曲',
|
||||
'artist': '周杰伦',
|
||||
'confidence': 0.8,
|
||||
'validation_count': 1,
|
||||
},
|
||||
candidate: candidate(),
|
||||
title: '夜曲',
|
||||
artist: '周杰伦',
|
||||
);
|
||||
expect(d.status, 'verified');
|
||||
expect(d.reason, 'stable');
|
||||
expect(d.validationCount, 2);
|
||||
});
|
||||
|
||||
test('标题或艺术家冲突 → 次数重置为 1,保持 pending', () {
|
||||
final d = MetadataStability.decide(
|
||||
previous: {
|
||||
'title': '夜曲',
|
||||
'artist': '周杰伦',
|
||||
'confidence': 0.9,
|
||||
'validation_count': 5,
|
||||
},
|
||||
candidate: candidate(),
|
||||
title: '完全不同的歌',
|
||||
artist: '别的歌手',
|
||||
);
|
||||
expect(d.status, 'pending');
|
||||
expect(d.reason, 'conflict');
|
||||
expect(d.validationCount, 1);
|
||||
});
|
||||
|
||||
test('置信度不足 → pending,但次数继续累计', () {
|
||||
final d = MetadataStability.decide(
|
||||
previous: {
|
||||
'title': '夜曲',
|
||||
'artist': '周杰伦',
|
||||
'confidence': 0.4,
|
||||
'validation_count': 3,
|
||||
},
|
||||
candidate: candidate(confidence: 0.5),
|
||||
title: '夜曲',
|
||||
artist: '周杰伦',
|
||||
);
|
||||
expect(d.status, 'pending');
|
||||
expect(d.reason, 'low_confidence');
|
||||
expect(d.validationCount, 4);
|
||||
});
|
||||
|
||||
test('一致但只验证过一次 → pending', () {
|
||||
final d = MetadataStability.decide(
|
||||
previous: {
|
||||
'title': '夜曲',
|
||||
'artist': '周杰伦',
|
||||
'confidence': 0.8,
|
||||
'validation_count': 0,
|
||||
},
|
||||
candidate: candidate(),
|
||||
title: '夜曲',
|
||||
artist: '周杰伦',
|
||||
);
|
||||
expect(d.status, 'pending');
|
||||
expect(d.reason, 'need_second_validation');
|
||||
expect(d.validationCount, 1);
|
||||
});
|
||||
|
||||
test('字段缺失不算冲突(旧记录没有艺术家)', () {
|
||||
final d = MetadataStability.decide(
|
||||
previous: {
|
||||
'title': '夜曲',
|
||||
'artist': '',
|
||||
'confidence': 0.7,
|
||||
'validation_count': 1,
|
||||
},
|
||||
candidate: candidate(),
|
||||
title: '夜曲',
|
||||
artist: '周杰伦',
|
||||
);
|
||||
expect(d.status, 'verified');
|
||||
expect(d.validationCount, 2);
|
||||
});
|
||||
|
||||
test('大小写与首尾空格不影响一致性判断', () {
|
||||
final d = MetadataStability.decide(
|
||||
previous: {
|
||||
'title': ' Night Song ',
|
||||
'artist': 'JAY',
|
||||
'confidence': 0.8,
|
||||
'validation_count': 1,
|
||||
},
|
||||
candidate: candidate(),
|
||||
title: 'night song',
|
||||
artist: 'jay',
|
||||
);
|
||||
expect(d.status, 'verified');
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user